diff --git a/app/.experimenter.yaml b/app/.experimenter.yaml index cbb293e759..b8cdf14cfe 100644 --- a/app/.experimenter.yaml +++ b/app/.experimenter.yaml @@ -58,6 +58,9 @@ juno-onboarding: cards: type: json description: Collection of user facing onboarding cards. + conditions: + type: json + description: "A collection of out the box conditional expressions to be used in determining whether a card should show or not. Each entry maps to a valid JEXL expression.\n" messaging: description: "The in-app messaging system.\n" hasExposure: true diff --git a/app/build.gradle b/app/build.gradle index 6ac92fc83a..2d47e3a7be 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -248,6 +248,15 @@ android { } bundle { + // Profiler issues require us to temporarily package native code compressed to + // match the previous APK packaging. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1865634 + packagingOptions { + jniLibs { + it.useLegacyPackaging = true + } + } + language { // Because we have runtime language selection we will keep all strings and languages // in the base APKs. @@ -547,7 +556,7 @@ dependencies { implementation ComponentsDependencies.androidx_coordinatorlayout implementation FenixDependencies.google_accompanist_drawablepainter - implementation ComponentsDependencies.thirdparty_sentry_latest + implementation ComponentsDependencies.thirdparty_sentry implementation project(':compose-awesomebar') implementation project(':compose-cfr') diff --git a/app/metrics.yaml b/app/metrics.yaml index 5dafd0685b..140f367116 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -7489,6 +7489,25 @@ first_session: - Performance - Attribution play_store_attribution: + install_referrer_response: + type: text + send_in_pings: + - first-session + description: | + The full install referrer response. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1862737 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/4343 + data_sensitivity: + # - technical + - web_activity # This is a workaround so we can use Text type for technical data. + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Attribution source: type: string send_in_pings: @@ -7611,6 +7630,80 @@ play_store_attribution: tags: - Attribution - Performance +meta_attribution: + app: + type: string + send_in_pings: + - first-session + description: | + The mobile application ID in Meta's attribution. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1860133 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/4171 + data_sensitivity: + - technical + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Attribution + t: + type: string + send_in_pings: + - first-session + description: | + Value tracking user interaction with Meta attribution. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1860133 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/4171 + data_sensitivity: + - technical + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Attribution + data: + type: text + send_in_pings: + - first-session + description: | + The Meta attribution data in encrypted format. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1860133 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/4171 + data_sensitivity: + # - technical + - web_activity # This is a workaround so we can use Text type for technical data. + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Attribution + nonce: + type: string + send_in_pings: + - first-session + description: | + Nonce used to decrypt the encrypted Meta attribution data. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1860133 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/4171 + data_sensitivity: + - technical + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Attribution browser.search: with_ads: type: labeled_counter @@ -8296,27 +8389,10 @@ autoplay: - SitePermissions cookie_banners: - visited_setting: - type: event - description: A user visited the cookie banner handling screen - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1796146 - data_reviews: - - https://github.com/mozilla-mobile/fenix/pull/27561 - - https://github.com/mozilla-mobile/firefox-android/pull/2597 - - https://github.com/mozilla-mobile/firefox-android/pull/4039 - data_sensitivity: - - interaction - notification_emails: - - android-probes@mozilla.com - expires: never - metadata: - tags: - - Privacy&Security - setting_changed: + setting_changed_pmb: type: event description: | - A user changed their setting. + A user changed their setting in private mode. extra_keys: cookie_banner_setting: description: | @@ -8392,35 +8468,23 @@ cookie_banners: metadata: tags: - Privacy&Security - visited_re_engagement_dialog: - type: event - description: An user visited the cookie banner re-engagement dialog - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1797593 - data_reviews: - - https://github.com/mozilla-mobile/fenix/pull/28405#issuecomment-1372489596 - - https://github.com/mozilla-mobile/firefox-android/pull/2597 - - https://github.com/mozilla-mobile/firefox-android/pull/4039 - data_sensitivity: - - interaction - notification_emails: - - android-probes@mozilla.com - expires: never - metadata: - tags: - - Privacy&Security - opt_out_re_engagement_dialog: - type: event + report_site_domain: + type: url description: | - An user opt out the cookie banner re-engagement - dialog by clicking the X button + A user can report a site domain(Ex. for https://edition.cnn.com/ + site domain will be cnn.com) when the cookie banner reducer is not + working from the cookie banner details panel. + lifetime: ping + send_in_pings: + - cookie-banner-report-site bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1797593 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1805450 data_reviews: - - https://github.com/mozilla-mobile/fenix/pull/28405#issuecomment-1372489596 - - https://github.com/mozilla-mobile/firefox-android/pull/2597 + - https://github.com/mozilla-mobile/firefox-android/pull/1298#pullrequestreview-1350344223 + - https://github.com/mozilla-mobile/firefox-android/pull/3319 - https://github.com/mozilla-mobile/firefox-android/pull/4039 data_sensitivity: + - technical - interaction notification_emails: - android-probes@mozilla.com @@ -8428,16 +8492,16 @@ cookie_banners: metadata: tags: - Privacy&Security - not_now_re_engagement_dialog: + report_site_cancel_button: type: event description: | - An user clicked the not now button on - the cookie banner re-engagement dialog + The user has pressed the report site domain cancel button + from the cookie banner reducer details panel. bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1797593 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1805450 data_reviews: - - https://github.com/mozilla-mobile/fenix/pull/28405#issuecomment-1372489596 - - https://github.com/mozilla-mobile/firefox-android/pull/2597 + - https://github.com/mozilla-mobile/firefox-android/pull/1298#pullrequestreview-1350344223 + - https://github.com/mozilla-mobile/firefox-android/pull/3319 - https://github.com/mozilla-mobile/firefox-android/pull/4039 data_sensitivity: - interaction @@ -8447,34 +8511,11 @@ cookie_banners: metadata: tags: - Privacy&Security - allow_re_engagement_dialog: + report_domain_site_button: type: event description: | - An user clicked the dismiss banner button - on the cookie banner re-engagement dialog - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1797593 - data_reviews: - - https://github.com/mozilla-mobile/fenix/pull/28405#issuecomment-1372489596 - - https://github.com/mozilla-mobile/firefox-android/pull/2597 - - https://github.com/mozilla-mobile/firefox-android/pull/4039 - data_sensitivity: - - interaction - notification_emails: - - android-probes@mozilla.com - expires: never - metadata: - tags: - - Privacy&Security - report_site_domain: - type: url - description: | - A user can report a site domain(Ex. for https://edition.cnn.com/ - site domain will be cnn.com) when the cookie banner reducer is not - working from the cookie banner details panel. - lifetime: ping - send_in_pings: - - cookie-banner-report-site + The user has pressed the report site domain button + from the cookie banner reducer details panel. bugs: - https://bugzilla.mozilla.org/show_bug.cgi?id=1805450 data_reviews: @@ -8482,7 +8523,6 @@ cookie_banners: - https://github.com/mozilla-mobile/firefox-android/pull/3319 - https://github.com/mozilla-mobile/firefox-android/pull/4039 data_sensitivity: - - technical - interaction notification_emails: - android-probes@mozilla.com @@ -8490,17 +8530,13 @@ cookie_banners: metadata: tags: - Privacy&Security - report_site_cancel_button: + cfr_shown: type: event - description: | - The user has pressed the report site domain cancel button - from the cookie banner reducer details panel. + description: The cookie banner cfr has been shown bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1805450 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1859393 data_reviews: - - https://github.com/mozilla-mobile/firefox-android/pull/1298#pullrequestreview-1350344223 - - https://github.com/mozilla-mobile/firefox-android/pull/3319 - - https://github.com/mozilla-mobile/firefox-android/pull/4039 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1859393#c2 data_sensitivity: - interaction notification_emails: @@ -8509,17 +8545,15 @@ cookie_banners: metadata: tags: - Privacy&Security - report_domain_site_button: + cfr_dismissal: type: event description: | - The user has pressed the report site domain button - from the cookie banner reducer details panel. + The cookie banners CFR was dismissed by the user by interacting + with the outside of the popup bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1805450 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1859393 data_reviews: - - https://github.com/mozilla-mobile/firefox-android/pull/1298#pullrequestreview-1350344223 - - https://github.com/mozilla-mobile/firefox-android/pull/3319 - - https://github.com/mozilla-mobile/firefox-android/pull/4039 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1859393#c2 data_sensitivity: - interaction notification_emails: @@ -8528,7 +8562,6 @@ cookie_banners: metadata: tags: - Privacy&Security - site_permissions: prompt_shown: type: event @@ -10812,6 +10845,126 @@ shopping: metadata: tags: - Shopping + surface_stale_analysis_shown: + type: event + description: | + Records an event when the "New info to check" card is shown. + send_in_pings: + - events + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1862776 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/4379#issuecomment-1794925138 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Shopping + surface_ads_impression: + type: event + description: | + The user viewed an ad in review checker for at least 1.5 seconds. + send_in_pings: + - events + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1865854 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/4604#issuecomment-1827890623 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Shopping + surface_ads_clicked: + type: event + description: | + The user clicked an ad in review checker. + send_in_pings: + - events + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1865854 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/4604#issuecomment-1827890623 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Shopping + surface_ads_setting_toggled: + type: event + description: | + The user toggled the ads display setting. + send_in_pings: + - events + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1865854 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/4604#issuecomment-1827890623 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + extra_keys: + action: + type: string + description: | + Whether the toggle was used to enable or disable ads. Possible values + are `enabled` and `disabled`. + metadata: + tags: + - Shopping + ads_exposure: + type: event + description: | + On a supported product page, the review checker showed analysis, + and the ads exposure pref was enabled, or review checker ads were enabled, + and when we tried to fetch an ad from the ad server, an ad was available. + Does not indicate whether the ad was actually shown. + send_in_pings: + - events + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1866992 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/4622#issuecomment-1829905076 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Shopping + surface_no_ads_available: + type: event + description: | + On a supported product page, the review checker + showed analysis, and review checker ads were enabled, + but when we tried to fetch an ad from the ad server, + no ad was available. + send_in_pings: + - events + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1866992 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/4622#issuecomment-1829905076 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Shopping shopping.settings: component_opted_out: @@ -10871,6 +11024,24 @@ shopping.settings: metadata: tags: - Shopping + disabled_ads: + type: boolean + description: | + Indicates if the user has disabled ads. + send_in_pings: + - metrics + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1865854 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/4604#issuecomment-1827890623 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Shopping fx_suggest: ping_type: type: string diff --git a/app/nimbus.fml.yaml b/app/nimbus.fml.yaml index a9875de9b7..8e535c9276 100644 --- a/app/nimbus.fml.yaml +++ b/app/nimbus.fml.yaml @@ -234,7 +234,38 @@ features: "jump-back-in-cfr": true, } } - + query-parameter-stripping: + description: Features for query parameter stripping. + variables: + sections-enabled: + description: "This property provides a lookup table of whether or not the given section should be enabled." + type: Map + default: + { + "query-parameter-stripping": "0", + "query-parameter-stripping-pmb": "0", + "query-parameter-stripping-allow-list": "", + "query-parameter-stripping-strip-list": "", + } + defaults: + - channel: developer + value: { + "sections-enabled": { + "query-parameter-stripping": "0", + "query-parameter-stripping-pmb": "0", + "query-parameter-stripping-allow-list": "", + "query-parameter-stripping-strip-list": "", + } + } + - channel: nightly + value: { + "sections-enabled": { + "query-parameter-stripping": "0", + "query-parameter-stripping-pmb": "0", + "query-parameter-stripping-allow-list": "", + "query-parameter-stripping-strip-list": "", + } + } cookie-banners: description: Features for cookie banner handling. variables: @@ -245,8 +276,10 @@ features: { "feature-ui": 0, "feature-setting-value": 0, - "dialog-re-engage-time": 4, - "feature-setting-value-pbm": 0 + "feature-setting-value-pbm": 0, + "feature-setting-detect-only": 0, + "feature-setting-global-rules": 0, + "feature-setting-global-rules-sub-frames": 0, } defaults: - channel: developer @@ -254,8 +287,10 @@ features: "sections-enabled": { "feature-ui": 1, "feature-setting-value": 0, - "dialog-re-engage-time": 4, - "feature-setting-value-pbm": 0 + "feature-setting-value-pbm": 1, + "feature-setting-detect-only": 0, + "feature-setting-global-rules": 0, + "feature-setting-global-rules-sub-frames": 0, } } - channel: nightly @@ -263,8 +298,21 @@ features: "sections-enabled": { "feature-ui": 1, "feature-setting-value": 0, - "dialog-re-engage-time": 4, - "feature-setting-value-pbm": 0 + "feature-setting-value-pbm": 1, + "feature-setting-detect-only": 0, + "feature-setting-global-rules": 0, + "feature-setting-global-rules-sub-frames": 0, + } + } + - channel: beta + value: { + "sections-enabled": { + "feature-ui": 1, + "feature-setting-value": 0, + "feature-setting-value-pbm": 1, + "feature-setting-detect-only": 0, + "feature-setting-global-rules": 0, + "feature-setting-global-rules-sub-frames": 0, } } unified-search: @@ -359,10 +407,16 @@ features: description: if true, recommended products feature is enabled to be shown to the user based on their preference. type: Boolean default: false + product-recommendations-exposure: + description: if true, we want to record recommended products inventory for opted-in users, even if product recommendations are disabled. + type: Boolean + default: false defaults: - channel: developer value: enabled: true + product-recommendations: true + product-recommendations-exposure: true print: description: A feature for printing from the share or browser menu. @@ -461,13 +515,38 @@ types: feature-setting-value: description: An integer either 0 or 1 indicating if cookie banner setting should be enabled or disabled, 0 for setting the value to disabled, 1 for enabling the setting with the value reject_all. - dialog-re-engage-time: - description: An integer indicating the number of hours that needs to happen before - the re-engagement dialog shows again since the last seen, for example if set to 4 - that means if the users has seen the dialog, it will see it 4 hours later. feature-setting-value-pbm: description: An integer either 0 or 1 indicating if cookie banner setting should be enabled or disabled, 0 for setting the value to disabled, 1 for enabling the setting with the value reject_all. + feature-setting-detect-only: + description: An integer either 0 or 1 indicating if cookie banner detect only mode + should be enabled or disabled. 0 for setting to be disabled, and 1 for enabling the setting. + feature-setting-global-rules: + description: An integer either 0 or 1 indicating if cookie banner global rules + should be enabled or disabled. 0 for setting to be disabled, and 1 for enabling the setting. + feature-setting-global-rules-sub-frames: + description: An integer either 0 or 1 indicating if cookie banner global rules sub-frames + should be enabled or disabled. 0 for setting to be disabled, and 1 for enabling the setting. + + QueryParameterStrippingSection: + description: The identifiers for the options for the Query Parameter Stripping feature. + variants: + query-parameter-stripping: + description: An integer either 0 or 1 indicating if query parameter stripping + should be enabled or disabled in normal mode. 0 for setting to be disabled, + and 1 for enabling the setting. + query-parameter-stripping-pmb: + description: An integer either 0 or 1 indicating if query parameter stripping + should be enabled or disabled in private mode. 0 for setting to be disabled, + and 1 for enabling the setting. + query-parameter-stripping-allow-list: + description: An string separated by commas indicating the sites where should + from query stripping should be exempted. + query-parameter-stripping-strip-list: + description: An string separated by commas indicating the list of query params + to be stripped from URIs. This list will be merged with records + coming from RemoteSettings. + OnboardingPanel: description: The types of onboarding panels in the onboarding page variants: diff --git a/app/onboarding.fml.yaml b/app/onboarding.fml.yaml index 1b9b158951..237a622c0d 100644 --- a/app/onboarding.fml.yaml +++ b/app/onboarding.fml.yaml @@ -5,6 +5,16 @@ features: description: A feature that shows juno onboarding flow. variables: + conditions: + description: > + A collection of out the box conditional expressions to be + used in determining whether a card should show or not. + Each entry maps to a valid JEXL expression. + type: Map + default: { + ALWAYS: "true", + NEVER: "false" + } cards: description: Collection of user facing onboarding cards. type: Map @@ -98,6 +108,20 @@ objects: description: The text to display on the secondary button. # This should never be defaulted. default: "" + prerequisites: + type: List + description: > + A list of strings corresponding to targeting expressions. + The card will be shown if all expressions are `true` and if + no expressions in the `disqualifiers` table are true, or + if the `disqualifiers` table is empty. + default: [ ALWAYS ] + disqualifiers: + type: List + description: > + A list of strings corresponding to targeting expressions. + The card will not be shown if any expression is `true`. + default: [ NEVER ] enums: diff --git a/app/src/androidTest/java/org/mozilla/fenix/components/FxaServer.kt b/app/src/androidTest/java/org/mozilla/fenix/components/FxaServer.kt index bb0c3b6876..2320fd50ee 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/components/FxaServer.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/components/FxaServer.kt @@ -5,8 +5,8 @@ package org.mozilla.fenix.components import android.content.Context -import mozilla.components.service.fxa.ServerConfig -import mozilla.components.service.fxa.ServerConfig.Server +import mozilla.appservices.fxaclient.FxaConfig +import mozilla.appservices.fxaclient.FxaServer /** * Utility to configure Firefox Account stage servers. @@ -17,7 +17,7 @@ object FxaServer { private const val REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel" @Suppress("UNUSED_PARAMETER") - fun config(context: Context): ServerConfig { - return ServerConfig(Server.STAGE, CLIENT_ID, REDIRECT_URL) + fun config(context: Context): FxaConfig { + return FxaConfig(FxaServer.Stage, CLIENT_ID, REDIRECT_URL) } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/GenericExperimentIntegrationTest.kt b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/GenericExperimentIntegrationTest.kt index ed5fa75c95..9bd0be1044 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/GenericExperimentIntegrationTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/GenericExperimentIntegrationTest.kt @@ -33,7 +33,8 @@ class GenericExperimentIntegrationTest { TestHelper.appContext.settings().showSecretDebugMenuThisSession = false } - private fun disableStudiesViaStudiesToggle() { + @Test + fun disableStudiesViaStudiesToggle() { homeScreen { }.openThreeDotMenu { }.openSettings { diff --git a/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/Pipfile b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/Pipfile index d21a2e8f6d..f475ca53c6 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/Pipfile +++ b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/Pipfile @@ -1,9 +1,14 @@ +# 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/. + [[source]] url = "https://pypi.python.org/simple" verify_ssl = true name = "pypi" [packages] +pydantic = "*" pytest = "*" pytest-html = "*" pytest-metadata = "*" diff --git a/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/Pipfile.lock b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/Pipfile.lock index d6e5d7db09..8665746c54 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/Pipfile.lock +++ b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "53501c7e751ae79697bf8c7289b6095f49fed97242fe186fea42989e800c39d5" + "sha256": "6dae5ac51aa7817578a25597da1ef783475050538443ba344c88a78969e68fd9" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,14 @@ ] }, "default": { + "annotated-types": { + "hashes": [ + "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43", + "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d" + ], + "markers": "python_version >= '3.8'", + "version": "==0.6.0" + }, "certifi": { "hashes": [ "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", @@ -226,23 +234,144 @@ "markers": "python_version >= '3.8'", "version": "==1.3.0" }, + "pydantic": { + "hashes": [ + "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7", + "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.4.2" + }, + "pydantic-core": { + "hashes": [ + "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e", + "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33", + "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7", + "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7", + "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea", + "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4", + "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0", + "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7", + "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94", + "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff", + "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82", + "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd", + "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893", + "sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e", + "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d", + "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901", + "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9", + "sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c", + "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7", + "sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891", + "sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f", + "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a", + "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9", + "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5", + "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e", + "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a", + "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c", + "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f", + "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514", + "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b", + "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302", + "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096", + "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0", + "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27", + "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884", + "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a", + "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357", + "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430", + "sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221", + "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325", + "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4", + "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05", + "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55", + "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875", + "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970", + "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc", + "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6", + "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f", + "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b", + "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d", + "sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15", + "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118", + "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee", + "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e", + "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6", + "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208", + "sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede", + "sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3", + "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e", + "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada", + "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175", + "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a", + "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c", + "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f", + "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58", + "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f", + "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a", + "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a", + "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921", + "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e", + "sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904", + "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776", + "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52", + "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf", + "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8", + "sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f", + "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b", + "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63", + "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c", + "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f", + "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468", + "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e", + "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab", + "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2", + "sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb", + "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb", + "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132", + "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b", + "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607", + "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934", + "sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698", + "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e", + "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561", + "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de", + "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b", + "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a", + "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595", + "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402", + "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881", + "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429", + "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5", + "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7", + "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c", + "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531", + "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6", + "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521" + ], + "markers": "python_version >= '3.7'", + "version": "==2.10.1" + }, "pytest": { "hashes": [ - "sha256:2f2301e797521b23e4d2585a0a3d7b5e50fdddaaf7e7d6773ea26ddb17c213ab", - "sha256:460c9a59b14e27c602eb5ece2e47bec99dc5fc5f6513cf924a7d03a578991b1f" + "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002", + "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==7.4.1" + "version": "==7.4.2" }, "pytest-html": { "hashes": [ - "sha256:3b473cc278272f8b5a34cd3bf10f88ac5fcb17cb5af22f9323514af00c310e64", - "sha256:79c4677ed6196417bf290d8b81f706342ae49f726f623728efa3f7dfff09f8eb" + "sha256:88682b9e8e51392472546a70a2139b27d6bc1834a4afd3e41da33c9d9f91e4a4", + "sha256:907c3e68462df129d3ee96dee58bd63f70216b06421836b22fd3fd57ef314acb" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.0.0" + "version": "==4.0.2" }, "pytest-metadata": { "hashes": [ @@ -328,12 +457,19 @@ "markers": "python_version >= '3.7'", "version": "==2.31.0" }, + "typing-extensions": { + "hashes": [ + "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", + "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" + ], + "markers": "python_version >= '3.8'", + "version": "==4.8.0" + }, "urllib3": { "hashes": [ "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84", "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e" ], - "index": "pypi", "markers": "python_version >= '3.7'", "version": "==2.0.7" } @@ -341,32 +477,28 @@ "develop": { "black": { "hashes": [ - "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3", - "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb", - "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087", - "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320", - "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6", - "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3", - "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc", - "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f", - "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587", - "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91", - "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a", - "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad", - "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926", - "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9", - "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be", - "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd", - "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96", - "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491", - "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2", - "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a", - "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f", - "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995" + "sha256:0e232f24a337fed7a82c1185ae46c56c4a6167fb0fe37411b43e876892c76699", + "sha256:30b78ac9b54cf87bcb9910ee3d499d2bc893afd52495066c49d9ee6b21eee06e", + "sha256:31946ec6f9c54ed7ba431c38bc81d758970dd734b96b8e8c2b17a367d7908171", + "sha256:31b9f87b277a68d0e99d2905edae08807c007973eaa609da5f0c62def6b7c0bd", + "sha256:47c4510f70ec2e8f9135ba490811c071419c115e46f143e4dce2ac45afdcf4c9", + "sha256:481167c60cd3e6b1cb8ef2aac0f76165843a374346aeeaa9d86765fe0dd0318b", + "sha256:6901631b937acbee93c75537e74f69463adaf34379a04eef32425b88aca88a23", + "sha256:76baba9281e5e5b230c9b7f83a96daf67a95e919c2dfc240d9e6295eab7b9204", + "sha256:7fb5fc36bb65160df21498d5a3dd330af8b6401be3f25af60c6ebfe23753f747", + "sha256:960c21555be135c4b37b7018d63d6248bdae8514e5c55b71e994ad37407f45b8", + "sha256:a3c2ddb35f71976a4cfeca558848c2f2f89abc86b06e8dd89b5a65c1e6c0f22a", + "sha256:c870bee76ad5f7a5ea7bd01dc646028d05568d33b0b09b7ecfc8ec0da3f3f39c", + "sha256:d3d9129ce05b0829730323bdcb00f928a448a124af5acf90aa94d9aba6969604", + "sha256:db451a3363b1e765c172c3fd86213a4ce63fb8524c938ebd82919bf2a6e28c6a", + "sha256:e223b731a0e025f8ef427dd79d8cd69c167da807f5710add30cdf131f13dd62e", + "sha256:f20ff03f3fdd2fd4460b4f631663813e57dc277e37fb216463f3b907aa5a9bdd", + "sha256:f74892b4b836e5162aa0452393112a574dac85e13902c57dfbaaf388e4eda37c", + "sha256:f8dc7d50d94063cdfd13c82368afd8588bac4ce360e4224ac399e769d6704e98" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==23.7.0" + "version": "==23.10.0" }, "click": { "hashes": [ diff --git a/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/conftest.py b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/conftest.py index 15c670fda4..3673031841 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/conftest.py +++ b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/conftest.py @@ -1,3 +1,7 @@ +# 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/. + import json import logging import os @@ -9,6 +13,7 @@ import pytest import requests from experimentintegration.gradlewbuild import GradlewBuild +from experimentintegration.models.models import TelemetryModel KLAATU_SERVER_URL = "http://localhost:1378" KLAATU_LOCAL_SERVER_URL = "http://localhost:1378" @@ -25,7 +30,6 @@ def pytest_addoption(parser): ) - @pytest.fixture(name="load_branches") def fixture_load_branches(experiment_url): branches = [] @@ -68,7 +72,7 @@ def fixture_experiment_data(experiment_url): item["style"] = "URGENT" for count, trigger in enumerate(item["trigger"]): if "USER_EN_SPEAKER" not in trigger: - del(item["trigger"][count]) + del item["trigger"][count] return [data] @@ -152,14 +156,55 @@ def fixture_send_test_results(): with open(f"{here.resolve()}/results/index.html", "rb") as f: files = {"file": f} try: - requests.post(f"{KLAATU_SERVER_URL}/test_results", files=files) + requests.post(f"{KLAATU_SERVER_URL}/test_results", files=files) except requests.exceptions.ConnectionError: pass +@pytest.fixture(name="check_ping_for_experiment") +def fixture_check_ping_for_experiment(experiment_slug, variables): + def _check_ping_for_experiment( + branch=None, experiment=experiment_slug, reason=None + ): + model = TelemetryModel(branch=branch, experiment=experiment) + + timeout = time.time() + 60 * 5 + while time.time() < timeout: + data = requests.get(f"{variables['urls']['telemetry_server']}/pings").json() + events = [] + for item in data: + event_items = item.get("events") + if event_items: + for event in event_items: + if ( + "category" in event + and "nimbus_events" in event["category"] + and "extra" in event + and "branch" in event["extra"] + ): + events.append(event) + for event in events: + event_name = event.get("name") + if (reason == "enrollment" and event_name == "enrollment") or ( + reason == "unenrollment" + and event_name in ["unenrollment", "disqualification"] + ): + telemetry_model = TelemetryModel( + branch=event["extra"]["branch"], + experiment=event["extra"]["experiment"], + ) + if model == telemetry_model: + return True + time.sleep(5) + return False + + return _check_ping_for_experiment + + @pytest.fixture(name="setup_experiment") -def fixture_setup_experiment(experiment_slug, json_data, gradlewbuild_log): +def fixture_setup_experiment(experiment_slug, json_data, gradlewbuild_log, variables): def _(branch): + requests.delete(f"{variables['urls']['telemetry_server']}/pings") logging.info(f"Testing experiment {experiment_slug}, BRANCH: {branch[0]}") command = f"nimbus-cli --app fenix --channel developer enroll {experiment_slug} --branch {branch[0]} --file {json_data} --reset-app" logging.info(f"Running command {command}") diff --git a/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/generate_smoke_tests.py b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/generate_smoke_tests.py new file mode 100644 index 0000000000..3114304859 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/generate_smoke_tests.py @@ -0,0 +1,82 @@ +from pathlib import Path +import subprocess + +import yaml + + +def search_for_smoke_tests(tests_name): + """Searches for smoke tests within the requested test module.""" + path = Path("../ui") + files = sorted([x for x in path.iterdir() if x.is_file()]) + locations = [] + file_name = None + test_names = [] + + for name in files: + if tests_name in name.name: + file_name = name + break + + with open(file_name, "r") as file: + code = file.read().split(" ") + code = [item for item in code if item != ""] + + for count, item in enumerate(code): + if "class" in item or "@SmokeTest" in item: + locations.append(count) + + for location in locations: + if len(test_names) == 0: + class_name = code[location + 1] + test_names.append(class_name) + else: + test_names.append(f"{class_name}#{code[location+3].strip('()')}") + return test_names + + +def create_test_file(): + """Create the python file to hold the tests.""" + + path = Path("tests/") + filename = "test_smoke_scenarios.py" + final_path = path / filename + + if final_path.exists(): + print("File Exists, you need to delete it to create a new one.") + return + # file exists + subprocess.run([f"touch {final_path}"], encoding="utf8", shell=True) + assert final_path.exists() + with open(final_path, "w") as file: + file.write("import pytest\n\n") + + +def generate_smoke_tests(tests_names=None): + """Generate pytest code for the requested tests.""" + pytest_file = "tests/test_smoke_scenarios.py" + tests = [] + + for test in tests_names[1:]: + test_name = test.replace("#", "_").lower() + tests.append( + f""" +@pytest.mark.smoke_test +def test_smoke_{test_name}(setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment): + setup_experiment(load_branches) + gradlewbuild.test("{test}", smoke=True) + assert check_ping_for_experiment +""" + ) + with open(pytest_file, "a") as file: + for item in tests: + file.writelines(f"{item}") + + +if __name__ == "__main__": + test_modules = None + create_test_file() + with open("variables.yaml", "r") as file: + test_modules = yaml.safe_load(file) + for item in test_modules.get("smoke_tests"): + tests = search_for_smoke_tests(item) + generate_smoke_tests(tests) diff --git a/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/gradlewbuild.py b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/gradlewbuild.py index 77fc18e58c..721d4ff3fa 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/gradlewbuild.py +++ b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/gradlewbuild.py @@ -16,12 +16,17 @@ class GradlewBuild(object): def __init__(self, log): self.log = log - def test(self, identifier): + def test(self, identifier, smoke=None): # self.adbrun.launch() # Change path accordingly to go to root folder to run gradlew os.chdir("../../../../../../../..") - cmd = f"adb shell am instrument -w -e class org.mozilla.fenix.experimentintegration.{identifier} org.mozilla.fenix.debug.test/androidx.test.runner.AndroidJUnitRunner" + test_type = "ui" if smoke else "experimentintegration" + cmd = f"adb shell am instrument -w -e class org.mozilla.fenix.{test_type}.{identifier} org.mozilla.fenix.debug.test/androidx.test.runner.AndroidJUnitRunner" + # if smoke: + # cmd = f"adb shell am instrument -w -e class org.mozilla.fenix.ui.{identifier} org.mozilla.fenix.debug.test/androidx.test.runner.AndroidJUnitRunner" + # else: + # cmd = f"adb shell am instrument -w -e class org.mozilla.fenix.experimentintegration.{identifier} org.mozilla.fenix.debug.test/androidx.test.runner.AndroidJUnitRunner" self.logger.info("Running cmd: {}".format(cmd)) diff --git a/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/models/__init__.py b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/models/__init__.py new file mode 100644 index 0000000000..6fbe8159b2 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/models/__init__.py @@ -0,0 +1,3 @@ +# 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/. diff --git a/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/models/models.py b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/models/models.py new file mode 100644 index 0000000000..c0ea72253f --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/models/models.py @@ -0,0 +1,14 @@ +# 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/. + +"""Data class Models""" + +from pydantic import BaseModel + + +class TelemetryModel(BaseModel): + """Experiment Telemetry model""" + + branch: str + experiment: str diff --git a/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/tests/test_generic_scenarios.py b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/tests/test_generic_scenarios.py index 51f4b46878..bcc841fefd 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/tests/test_generic_scenarios.py +++ b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/tests/test_generic_scenarios.py @@ -1,12 +1,17 @@ import pytest @pytest.mark.parametrize("load_branches", [("branch")], indirect=True) -def test_experiment_unenrolls_via_studies_toggle(setup_experiment, gradlewbuild, load_branches): +def test_experiment_unenrolls_via_studies_toggle(setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment): setup_experiment(load_branches) gradlewbuild.test("GenericExperimentIntegrationTest#disableStudiesViaStudiesToggle") - gradlewbuild.test("GenericExperimentIntegrationTest#testExperimentUnenrolls") + assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0]) + gradlewbuild.test("GenericExperimentIntegrationTest#testExperimentUnenrolled") + assert check_ping_for_experiment(reason="unenrollment", branch=load_branches[0]) @pytest.mark.parametrize("load_branches", [("branch")], indirect=True) -def test_experiment_unenrolls_via_secret_menu(setup_experiment, gradlewbuild, load_branches): +def test_experiment_unenrolls_via_secret_menu(setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment): setup_experiment(load_branches) - gradlewbuild.test("GenericExperimentIntegrationTest#testExperimentUnenrollsViaSecretMenu") + gradlewbuild.test("GenericExperimentIntegrationTest#testExperimentUnenrolledViaSecretMenu") + assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0]) + gradlewbuild.test("GenericExperimentIntegrationTest#testExperimentUnenrolled") + assert check_ping_for_experiment(reason="unenrollment", branch=load_branches[0]) diff --git a/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/tests/test_survey_messages.py b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/tests/test_survey_messages.py index 6046a42646..db08522a3e 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/tests/test_survey_messages.py +++ b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/tests/test_survey_messages.py @@ -1,22 +1,47 @@ +# 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/. + import pytest @pytest.mark.parametrize("load_branches", [("branch")], indirect=True) -def test_survey_navigates_correctly(setup_experiment, gradlewbuild, load_branches): +def test_survey_navigates_correctly( + setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment +): setup_experiment(load_branches) gradlewbuild.test("SurveyExperimentIntegrationTest#checkSurveyNavigatesCorrectly") + assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0]) + @pytest.mark.parametrize("load_branches", [("branch")], indirect=True) -def test_survey_no_thanks_navigates_correctly(setup_experiment, gradlewbuild, load_branches): +def test_survey_no_thanks_navigates_correctly( + setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment +): setup_experiment(load_branches) - gradlewbuild.test("SurveyExperimentIntegrationTest#checkSurveyNoThanksNavigatesCorrectly") + gradlewbuild.test( + "SurveyExperimentIntegrationTest#checkSurveyNoThanksNavigatesCorrectly" + ) + assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0]) + @pytest.mark.parametrize("load_branches", [("branch")], indirect=True) -def test_homescreen_survey_dismisses_correctly(setup_experiment, gradlewbuild, load_branches): +def test_homescreen_survey_dismisses_correctly( + setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment +): setup_experiment(load_branches) - gradlewbuild.test("SurveyExperimentIntegrationTest#checkHomescreenSurveyDismissesCorrectly") + gradlewbuild.test( + "SurveyExperimentIntegrationTest#checkHomescreenSurveyDismissesCorrectly" + ) + assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0]) + @pytest.mark.parametrize("load_branches", [("branch")], indirect=True) -def test_survey_landscape_looks_correct(setup_experiment, gradlewbuild, load_branches): +def test_survey_landscape_looks_correct( + setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment +): setup_experiment(load_branches) - gradlewbuild.test("SurveyExperimentIntegrationTest#checkSurveyLandscapeLooksCorrect") + gradlewbuild.test( + "SurveyExperimentIntegrationTest#checkSurveyLandscapeLooksCorrect" + ) + assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0]) diff --git a/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/variables.yaml b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/variables.yaml index 54e26e5024..286a36eae0 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/variables.yaml +++ b/app/src/androidTest/java/org/mozilla/fenix/experimentintegration/variables.yaml @@ -1,3 +1,10 @@ +# 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/. + urls: stage_server: "https://stage.experimenter.nonprod.dataops.mozgcp.net" prod_server: "https://experimenter.services.mozilla.com" + telemetry_server: "http://172.25.58.187:5000" +smoke_tests: + - "AddressAutofillTest" diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/AppAndSystemHelper.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/AppAndSystemHelper.kt new file mode 100644 index 0000000000..8e5be4de15 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/AppAndSystemHelper.kt @@ -0,0 +1,362 @@ +/* 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/. */ +@file:Suppress("DEPRECATION") + +package org.mozilla.fenix.helpers + +import android.Manifest +import android.app.ActivityManager +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.net.Uri +import android.os.Build +import android.os.storage.StorageManager +import android.os.storage.StorageVolume +import android.provider.Settings +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.test.espresso.Espresso +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.IdlingResource +import androidx.test.espresso.intent.Intents.intended +import androidx.test.espresso.intent.matcher.IntentMatchers.toPackage +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.ActivityTestRule +import androidx.test.runner.permission.PermissionRequester +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiObject +import androidx.test.uiautomator.UiSelector +import androidx.test.uiautomator.Until +import junit.framework.AssertionFailedError +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.mozilla.fenix.Config +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity +import org.mozilla.fenix.helpers.Constants.PackageName.YOUTUBE_APP +import org.mozilla.fenix.helpers.Constants.TAG +import org.mozilla.fenix.helpers.TestHelper.mDevice +import org.mozilla.fenix.helpers.ext.waitNotNull +import org.mozilla.fenix.helpers.idlingresource.NetworkConnectionIdlingResource +import org.mozilla.fenix.ui.robots.BrowserRobot +import org.mozilla.gecko.util.ThreadUtils +import java.io.File +import java.util.Locale + +object AppAndSystemHelper { + + fun getPermissionAllowID(): String { + return when + (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { + true -> "com.android.permissioncontroller" + false -> "com.android.packageinstaller" + } + } + + @RequiresApi(Build.VERSION_CODES.R) + fun deleteDownloadedFileOnStorage(fileName: String) { + val storageManager: StorageManager? = TestHelper.appContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager? + val storageVolumes = storageManager!!.storageVolumes + val storageVolume: StorageVolume = storageVolumes[0] + val file = File(storageVolume.directory!!.path + "/Download/" + fileName) + try { + if (file.exists()) { + file.delete() + Log.d("TestLog", "File delete try 1") + Assert.assertFalse("The file was not deleted", file.exists()) + } + } catch (e: AssertionError) { + file.delete() + Log.d("TestLog", "File delete retried") + Assert.assertFalse("The file was not deleted", file.exists()) + } + } + + @RequiresApi(Build.VERSION_CODES.R) + fun clearDownloadsFolder() { + val storageManager: StorageManager? = TestHelper.appContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager? + val storageVolumes = storageManager!!.storageVolumes + val storageVolume: StorageVolume = storageVolumes[0] + val downloadsFolder = File(storageVolume.directory!!.path + "/Download/") + + // Check if the downloads folder exists + if (downloadsFolder.exists() && downloadsFolder.isDirectory) { + Log.i(TAG, "clearDownloadsFolder: Verified that \"DOWNLOADS\" folder exists") + val files = downloadsFolder.listFiles() + + // Check if the folder is not empty + if (files != null && files.isNotEmpty()) { + Log.i(TAG, "clearDownloadsFolder: Verified that \"DOWNLOADS\" folder is not empty") + // Delete all files in the folder + for (file in files) { + file.delete() + Log.i(TAG, "clearDownloadsFolder: Deleted $file from \"DOWNLOADS\" folder") + } + } + } + } + + fun setNetworkEnabled(enabled: Boolean) { + val networkDisconnectedIdlingResource = NetworkConnectionIdlingResource(false) + val networkConnectedIdlingResource = NetworkConnectionIdlingResource(true) + + when (enabled) { + true -> { + mDevice.executeShellCommand("svc data enable") + mDevice.executeShellCommand("svc wifi enable") + + // Wait for network connection to be completely enabled + IdlingRegistry.getInstance().register(networkConnectedIdlingResource) + Espresso.onIdle { + IdlingRegistry.getInstance().unregister(networkConnectedIdlingResource) + } + Log.i(TAG, "setNetworkEnabled: Network connection was enabled") + } + + false -> { + mDevice.executeShellCommand("svc data disable") + mDevice.executeShellCommand("svc wifi disable") + + // Wait for network connection to be completely disabled + IdlingRegistry.getInstance().register(networkDisconnectedIdlingResource) + Espresso.onIdle { + IdlingRegistry.getInstance().unregister(networkDisconnectedIdlingResource) + } + Log.i(TAG, "setNetworkEnabled: Network connection was disabled") + } + } + } + + fun isPackageInstalled(packageName: String): Boolean { + return try { + val packageManager = InstrumentationRegistry.getInstrumentation().context.packageManager + packageManager.getApplicationInfo(packageName, 0).enabled + } catch (e: PackageManager.NameNotFoundException) { + Log.i(TAG, "isPackageInstalled: Catch block - ${e.message}") + false + } + } + + fun assertExternalAppOpens(appPackageName: String) { + if (isPackageInstalled(appPackageName)) { + Log.i(TAG, "assertExternalAppOpens: $appPackageName is installed on device") + try { + Log.i(TAG, "assertExternalAppOpens: Try block") + intended(toPackage(appPackageName)) + Log.i(TAG, "assertExternalAppOpens: Matched intent to $appPackageName") + } catch (e: AssertionFailedError) { + Log.i(TAG, "assertExternalAppOpens: Catch block - ${e.message}") + } + } else { + mDevice.waitNotNull( + Until.findObject(By.text("Could not open file")), + TestAssetHelper.waitingTime, + ) + Log.i(TAG, "assertExternalAppOpens: Verified \"Could not open file\" message") + } + } + + fun assertNativeAppOpens(appPackageName: String, url: String = "") { + if (isPackageInstalled(appPackageName)) { + mDevice.waitForIdle(TestAssetHelper.waitingTimeShort) + Assert.assertTrue( + TestHelper.mDevice.findObject(UiSelector().packageName(appPackageName)) + .waitForExists(TestAssetHelper.waitingTime), + ) + } else { + BrowserRobot().verifyUrl(url) + } + } + + fun assertYoutubeAppOpens() = intended(toPackage(YOUTUBE_APP)) + + /** + * Checks whether the latest activity of the application is used for custom tabs or PWAs. + * + * @return Boolean value that helps us know if the current activity supports custom tabs or PWAs. + */ + fun isExternalAppBrowserActivityInCurrentTask(): Boolean { + val activityManager = TestHelper.appContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + + mDevice.waitForIdle(TestAssetHelper.waitingTimeShort) + + return activityManager.appTasks[0].taskInfo.topActivity!!.className == ExternalAppBrowserActivity::class.java.name + } + + /** + * Run test with automatically registering idling resources and cleanup. + * + * @param idlingResources zero or more [IdlingResource] to be used when running [testBlock]. + * @param testBlock test code to execute. + */ + fun registerAndCleanupIdlingResources( + vararg idlingResources: IdlingResource, + testBlock: () -> Unit, + ) { + idlingResources.forEach { + IdlingRegistry.getInstance().register(it) + } + + try { + testBlock() + } finally { + idlingResources.forEach { + IdlingRegistry.getInstance().unregister(it) + } + } + } + + // Permission allow dialogs differ on various Android APIs + fun grantSystemPermission() { + val whileUsingTheAppPermissionButton: UiObject = + mDevice.findObject(UiSelector().textContains("While using the app")) + + val allowPermissionButton: UiObject = + mDevice.findObject( + UiSelector() + .textContains("Allow") + .className("android.widget.Button"), + ) + + if (Build.VERSION.SDK_INT >= 23) { + if (whileUsingTheAppPermissionButton.waitForExists(TestAssetHelper.waitingTimeShort)) { + whileUsingTheAppPermissionButton.click() + } else if (allowPermissionButton.waitForExists(TestAssetHelper.waitingTimeShort)) { + allowPermissionButton.click() + } + } + } + + // Permission deny dialogs differ on various Android APIs + fun denyPermission() { + mDevice.findObject(UiSelector().textContains("Deny")).waitForExists(TestAssetHelper.waitingTime) + mDevice.findObject(UiSelector().textContains("Deny")).click() + } + + fun isTestLab(): Boolean { + return Settings.System.getString(TestHelper.appContext.contentResolver, "firebase.test.lab").toBoolean() + } + + /** + * Changes the default language of the entire device, not just the app. + * Runs on Debug variant as we don't want to adjust Release permission manifests + * Runs the test in its testBlock. + * Cleans up and sets the default locale after it's done. + */ + fun runWithSystemLocaleChanged(locale: Locale, testRule: ActivityTestRule, testBlock: () -> Unit) { + if (Config.channel.isDebug) { + /* Sets permission to change device language */ + PermissionRequester().apply { + addPermissions( + Manifest.permission.CHANGE_CONFIGURATION, + ) + requestPermissions() + } + + val defaultLocale = Locale.getDefault() + + try { + setSystemLocale(locale) + testBlock() + ThreadUtils.runOnUiThread { testRule.activity.recreate() } + } catch (e: Exception) { + e.printStackTrace() + } finally { + setSystemLocale(defaultLocale) + } + } + } + + /** + * Changes the default language of the entire device, not just the app. + */ + fun setSystemLocale(locale: Locale) { + val activityManagerNative = Class.forName("android.app.ActivityManagerNative") + val am = activityManagerNative.getMethod("getDefault", *arrayOfNulls(0)) + .invoke(activityManagerNative, *arrayOfNulls(0)) + val config = InstrumentationRegistry.getInstrumentation().context.resources.configuration + config.javaClass.getDeclaredField("locale")[config] = locale + config.javaClass.getDeclaredField("userSetLocale").setBoolean(config, true) + am.javaClass.getMethod( + "updateConfiguration", + Configuration::class.java, + ).invoke(am, config) + } + + fun putAppToBackground() { + mDevice.pressRecentApps() + mDevice.findObject(UiSelector().resourceId("${TestHelper.packageName}:id/container")).waitUntilGone( + TestAssetHelper.waitingTime, + ) + } + + fun bringAppToForeground() { + mDevice.pressRecentApps() + mDevice.findObject(UiSelector().resourceId("${TestHelper.packageName}:id/container")).waitForExists( + TestAssetHelper.waitingTime, + ) + } + + fun verifyKeyboardVisibility(isExpectedToBeVisible: Boolean = true) { + mDevice.waitForIdle() + + assertEquals( + "Keyboard not shown", + isExpectedToBeVisible, + mDevice + .executeShellCommand("dumpsys input_method | grep mInputShown") + .contains("mInputShown=true"), + ) + } + + fun openAppFromExternalLink(url: String) { + val context = InstrumentationRegistry.getInstrumentation().getTargetContext() + val intent = Intent().apply { + action = Intent.ACTION_VIEW + data = Uri.parse(url) + `package` = TestHelper.packageName + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + try { + context.startActivity(intent) + } catch (ex: ActivityNotFoundException) { + intent.setPackage(null) + context.startActivity(intent) + } + } + + /** + * Wrapper for tests to run only when certain conditions are met. + * For example: this method will avoid accidentally running a test on GV versions where the feature is disabled. + */ + fun runWithCondition(condition: Boolean, testBlock: () -> Unit) { + if (condition) { + testBlock() + } + } + + /** + * Wrapper to launch the app using the launcher intent. + */ + fun runWithLauncherIntent( + activityTestRule: AndroidComposeTestRule, + testBlock: () -> Unit, + ) { + val launcherIntent = Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_LAUNCHER) + } + + activityTestRule.activityRule.withIntent(launcherIntent).launchActivity(launcherIntent) + + try { + testBlock() + } catch (e: Exception) { + e.printStackTrace() + } + } +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/Constants.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/Constants.kt index a7cf3e3156..450ef4beef 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/Constants.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/Constants.kt @@ -4,10 +4,13 @@ package org.mozilla.fenix.helpers -import org.mozilla.fenix.helpers.TestHelper.getSponsoredShortcutTitle +import org.mozilla.fenix.helpers.DataGenerationHelper.getSponsoredShortcutTitle object Constants { + // Tag used for logging + const val TAG = "MozUITestLog" + // Device or AVD requires a Google Services Android OS installation object PackageName { const val GOOGLE_PLAY_SERVICES = "com.android.vending" @@ -18,6 +21,7 @@ object Constants { const val GMAIL_APP = "com.google.android.gm" const val PHONE_APP = "com.android.dialer" const val ANDROID_SETTINGS = "com.android.settings" + const val PRINT_SPOOLER = "com.android.printspooler" } const val SPEECH_RECOGNITION = "android.speech.action.RECOGNIZE_SPEECH" diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/DataGenerationHelper.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/DataGenerationHelper.kt new file mode 100644 index 0000000000..b4edc8bc3f --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/DataGenerationHelper.kt @@ -0,0 +1,113 @@ +/* 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.helpers + +import android.app.PendingIntent +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.net.Uri +import androidx.browser.customtabs.CustomTabsIntent +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiSelector +import mozilla.components.browser.state.search.SearchEngine +import mozilla.components.browser.state.state.availableSearchEngines +import org.junit.Assert +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.helpers.TestHelper.mDevice +import org.mozilla.fenix.utils.IntentUtils + +object DataGenerationHelper { + val appContext: Context = InstrumentationRegistry.getInstrumentation().targetContext + + fun createCustomTabIntent( + pageUrl: String, + customMenuItemLabel: String = "", + customActionButtonDescription: String = "", + ): Intent { + val appContext = InstrumentationRegistry.getInstrumentation() + .targetContext + .applicationContext + val pendingIntent = PendingIntent.getActivity(appContext, 0, Intent(), IntentUtils.defaultIntentPendingFlags) + val customTabsIntent = CustomTabsIntent.Builder() + .addMenuItem(customMenuItemLabel, pendingIntent) + .setShareState(CustomTabsIntent.SHARE_STATE_ON) + .setActionButton( + createTestBitmap(), + customActionButtonDescription, + pendingIntent, + true, + ) + .build() + customTabsIntent.intent.data = Uri.parse(pageUrl) + return customTabsIntent.intent + } + + private fun createTestBitmap(): Bitmap { + val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + canvas.drawColor(Color.GREEN) + return bitmap + } + + fun getStringResource(id: Int, argument: String = TestHelper.appName) = TestHelper.appContext.resources.getString(id, argument) + + private val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') + fun generateRandomString(stringLength: Int) = + (1..stringLength) + .map { kotlin.random.Random.nextInt(0, charPool.size) } + .map(charPool::get) + .joinToString("") + + /** + * Creates clipboard data. + */ + fun setTextToClipBoard(context: Context, message: String) { + val clipBoard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clipData = ClipData.newPlainText("label", message) + + clipBoard.setPrimaryClip(clipData) + } + + /** + * Returns sponsored shortcut title based on the index. + */ + fun getSponsoredShortcutTitle(position: Int): String { + val sponsoredShortcut = mDevice.findObject( + UiSelector() + .resourceId("${TestHelper.packageName}:id/top_site_item") + .index(position - 1), + ).getChild( + UiSelector() + .resourceId("${TestHelper.packageName}:id/top_site_title"), + ).text + + return sponsoredShortcut + } + + /** + * The list of Search engines for the "home" region of the user. + * For en-us it will return the 6 engines selected by default: Google, Bing, DuckDuckGo, Amazon, Ebay, Wikipedia. + */ + fun getRegionSearchEnginesList(): List { + val searchEnginesList = appContext.components.core.store.state.search.regionSearchEngines + Assert.assertTrue("Search engines list returned nothing", searchEnginesList.isNotEmpty()) + return searchEnginesList + } + + /** + * The list of Search engines available to be added by user choice. + * For en-us it will return the 2 engines: Reddit, Youtube. + */ + fun getAvailableSearchEngines(): List { + val searchEnginesList = TestHelper.appContext.components.core.store.state.search.availableSearchEngines + Assert.assertTrue("Search engines list returned nothing", searchEnginesList.isNotEmpty()) + return searchEnginesList + } +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelper.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelper.kt index a42fe1a316..9b6fda9e1e 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelper.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelper.kt @@ -67,11 +67,6 @@ interface FeatureSettingsHelper { */ var etpPolicy: ETPPolicy - /** - * Enable or disable cookie banner reduction dialog. - */ - var isCookieBannerReductionDialogEnabled: Boolean - /** * Enable or disable open in app banner. */ diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelperDelegate.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelperDelegate.kt index c31fcf7560..9426d0c123 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelperDelegate.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelperDelegate.kt @@ -33,7 +33,6 @@ class FeatureSettingsHelperDelegate() : FeatureSettingsHelper { isTCPCFREnabled = settings.shouldShowTotalCookieProtectionCFR, isWallpaperOnboardingEnabled = settings.showWallpaperOnboarding, isDeleteSitePermissionsEnabled = settings.deleteSitePermissions, - isCookieBannerReductionDialogEnabled = !settings.userOptOutOfReEngageCookieBannerDialog, isOpenInAppBannerEnabled = settings.shouldShowOpenInAppBanner, etpPolicy = getETPPolicy(settings), tabsTrayRewriteEnabled = settings.enableTabsTrayToCompose, @@ -63,7 +62,6 @@ class FeatureSettingsHelperDelegate() : FeatureSettingsHelper { override var isRecentlyVisitedFeatureEnabled: Boolean by updatedFeatureFlags::isRecentlyVisitedFeatureEnabled override var isPWAsPromptEnabled: Boolean by updatedFeatureFlags::isPWAsPromptEnabled override var isTCPCFREnabled: Boolean by updatedFeatureFlags::isTCPCFREnabled - override var isCookieBannerReductionDialogEnabled: Boolean by updatedFeatureFlags::isCookieBannerReductionDialogEnabled override var isOpenInAppBannerEnabled: Boolean by updatedFeatureFlags::isOpenInAppBannerEnabled override var etpPolicy: ETPPolicy by updatedFeatureFlags::etpPolicy override var tabsTrayRewriteEnabled: Boolean by updatedFeatureFlags::tabsTrayRewriteEnabled @@ -90,7 +88,6 @@ class FeatureSettingsHelperDelegate() : FeatureSettingsHelper { settings.shouldShowTotalCookieProtectionCFR = featureFlags.isTCPCFREnabled settings.showWallpaperOnboarding = featureFlags.isWallpaperOnboardingEnabled settings.deleteSitePermissions = featureFlags.isDeleteSitePermissionsEnabled - settings.userOptOutOfReEngageCookieBannerDialog = !featureFlags.isCookieBannerReductionDialogEnabled settings.shouldShowOpenInAppBanner = featureFlags.isOpenInAppBannerEnabled settings.enableTabsTrayToCompose = featureFlags.tabsTrayRewriteEnabled settings.enableComposeTopSites = featureFlags.composeTopSitesEnabled @@ -109,7 +106,6 @@ private data class FeatureFlags( var isTCPCFREnabled: Boolean, var isWallpaperOnboardingEnabled: Boolean, var isDeleteSitePermissionsEnabled: Boolean, - var isCookieBannerReductionDialogEnabled: Boolean, var isOpenInAppBannerEnabled: Boolean, var etpPolicy: ETPPolicy, var tabsTrayRewriteEnabled: Boolean, diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt index 9243a102f9..754f7f5a5b 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt @@ -52,7 +52,6 @@ class HomeActivityTestRule( isTCPCFREnabled: Boolean = settings.shouldShowTotalCookieProtectionCFR, isWallpaperOnboardingEnabled: Boolean = settings.showWallpaperOnboarding, isDeleteSitePermissionsEnabled: Boolean = settings.deleteSitePermissions, - isCookieBannerReductionDialogEnabled: Boolean = !settings.userOptOutOfReEngageCookieBannerDialog, isOpenInAppBannerEnabled: Boolean = settings.shouldShowOpenInAppBanner, etpPolicy: ETPPolicy = getETPPolicy(settings), tabsTrayRewriteEnabled: Boolean = false, @@ -67,7 +66,6 @@ class HomeActivityTestRule( this.isTCPCFREnabled = isTCPCFREnabled this.isWallpaperOnboardingEnabled = isWallpaperOnboardingEnabled this.isDeleteSitePermissionsEnabled = isDeleteSitePermissionsEnabled - this.isCookieBannerReductionDialogEnabled = isCookieBannerReductionDialogEnabled this.isOpenInAppBannerEnabled = isOpenInAppBannerEnabled this.etpPolicy = etpPolicy this.tabsTrayRewriteEnabled = tabsTrayRewriteEnabled @@ -126,7 +124,6 @@ class HomeActivityTestRule( isPWAsPromptEnabled = false, isTCPCFREnabled = false, isWallpaperOnboardingEnabled = false, - isCookieBannerReductionDialogEnabled = false, isOpenInAppBannerEnabled = false, composeTopSitesEnabled = composeTopSitesEnabled, ) @@ -164,7 +161,6 @@ class HomeActivityIntentTestRule internal constructor( isTCPCFREnabled: Boolean = settings.shouldShowTotalCookieProtectionCFR, isWallpaperOnboardingEnabled: Boolean = settings.showWallpaperOnboarding, isDeleteSitePermissionsEnabled: Boolean = settings.deleteSitePermissions, - isCookieBannerReductionDialogEnabled: Boolean = !settings.userOptOutOfReEngageCookieBannerDialog, isOpenInAppBannerEnabled: Boolean = settings.shouldShowOpenInAppBanner, etpPolicy: ETPPolicy = getETPPolicy(settings), tabsTrayRewriteEnabled: Boolean = false, @@ -179,7 +175,6 @@ class HomeActivityIntentTestRule internal constructor( this.isTCPCFREnabled = isTCPCFREnabled this.isWallpaperOnboardingEnabled = isWallpaperOnboardingEnabled this.isDeleteSitePermissionsEnabled = isDeleteSitePermissionsEnabled - this.isCookieBannerReductionDialogEnabled = isCookieBannerReductionDialogEnabled this.isOpenInAppBannerEnabled = isOpenInAppBannerEnabled this.etpPolicy = etpPolicy this.tabsTrayRewriteEnabled = tabsTrayRewriteEnabled @@ -244,7 +239,6 @@ class HomeActivityIntentTestRule internal constructor( isTCPCFREnabled = settings.shouldShowTotalCookieProtectionCFR isWallpaperOnboardingEnabled = settings.showWallpaperOnboarding isDeleteSitePermissionsEnabled = settings.deleteSitePermissions - isCookieBannerReductionDialogEnabled = !settings.userOptOutOfReEngageCookieBannerDialog isOpenInAppBannerEnabled = settings.shouldShowOpenInAppBanner etpPolicy = getETPPolicy(settings) } @@ -275,7 +269,6 @@ class HomeActivityIntentTestRule internal constructor( isPWAsPromptEnabled = false, isTCPCFREnabled = false, isWallpaperOnboardingEnabled = false, - isCookieBannerReductionDialogEnabled = false, isOpenInAppBannerEnabled = false, composeTopSitesEnabled = composeTopSitesEnabled, ) diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/MatcherHelper.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/MatcherHelper.kt index ee4aa4471c..2ca012dfcd 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/MatcherHelper.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/MatcherHelper.kt @@ -4,10 +4,12 @@ package org.mozilla.fenix.helpers +import android.util.Log import androidx.test.uiautomator.UiObject import androidx.test.uiautomator.UiSelector import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue +import org.mozilla.fenix.helpers.Constants.TAG import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort import org.mozilla.fenix.helpers.TestHelper.mDevice @@ -17,101 +19,131 @@ import org.mozilla.fenix.helpers.TestHelper.mDevice */ object MatcherHelper { - fun itemWithResId(resourceId: String) = - mDevice.findObject(UiSelector().resourceId(resourceId)) + fun itemWithResId(resourceId: String): UiObject { + Log.i(TAG, "Looking for item with resource id: $resourceId") + return mDevice.findObject(UiSelector().resourceId(resourceId)) + } - fun itemContainingText(itemText: String) = - mDevice.findObject(UiSelector().textContains(itemText)) + fun itemContainingText(itemText: String): UiObject { + Log.i(TAG, "Looking for item with text: $itemText") + return mDevice.findObject(UiSelector().textContains(itemText)) + } - fun itemWithText(itemText: String) = - mDevice.findObject(UiSelector().text(itemText)) + fun itemWithText(itemText: String): UiObject { + Log.i(TAG, "Looking for item with text: $itemText") + return mDevice.findObject(UiSelector().text(itemText)) + } - fun itemWithDescription(description: String) = - mDevice.findObject(UiSelector().descriptionContains(description)) + fun itemWithDescription(description: String): UiObject { + Log.i(TAG, "Looking for item with description: $description") + return mDevice.findObject(UiSelector().descriptionContains(description)) + } - fun checkedItemWithResId(resourceId: String, isChecked: Boolean) = - mDevice.findObject(UiSelector().resourceId(resourceId).checked(isChecked)) + fun itemWithIndex(index: Int): UiObject { + Log.i(TAG, "Looking for item with index: $index") + return mDevice.findObject(UiSelector().index(index)) + } + + fun itemWithClassName(className: String): UiObject { + Log.i(TAG, "Looking for item with class name: $className") + return mDevice.findObject(UiSelector().className(className)) + } + + fun itemWithResIdAndIndex(resourceId: String, index: Int): UiObject { + Log.i(TAG, "Looking for item with resource id: $resourceId and index: $index") + return mDevice.findObject(UiSelector().resourceId(resourceId).index(index)) + } - fun checkedItemWithResIdAndText(resourceId: String, text: String, isChecked: Boolean) = - mDevice.findObject( + fun itemWithClassNameAndIndex(className: String, index: Int): UiObject { + Log.i(TAG, "Looking for item with class name: $className and index: $index") + return mDevice.findObject(UiSelector().className(className).index(index)) + } + + fun checkedItemWithResId(resourceId: String, isChecked: Boolean): UiObject { + Log.i(TAG, "Looking for checked item with resource id: $resourceId") + return mDevice.findObject(UiSelector().resourceId(resourceId).checked(isChecked)) + } + + fun checkedItemWithResIdAndText(resourceId: String, text: String, isChecked: Boolean): UiObject { + Log.i(TAG, "Looking for checked item with resource id: $resourceId and text: $text") + return mDevice.findObject( UiSelector() .resourceId(resourceId) .textContains(text) .checked(isChecked), ) + } - fun itemWithResIdAndDescription(resourceId: String, description: String) = - mDevice.findObject(UiSelector().resourceId(resourceId).descriptionContains(description)) - - fun itemWithResIdAndText(resourceId: String, text: String) = - mDevice.findObject(UiSelector().resourceId(resourceId).text(text)) - - fun itemWithResIdContainingText(resourceId: String, text: String) = - mDevice.findObject(UiSelector().resourceId(resourceId).textContains(text)) + fun itemWithResIdAndDescription(resourceId: String, description: String): UiObject { + Log.i(TAG, "Looking for item with resource id: $resourceId and description: $description") + return mDevice.findObject(UiSelector().resourceId(resourceId).descriptionContains(description)) + } - fun assertItemWithResIdExists(vararg appItems: UiObject, exists: Boolean = true) { - if (exists) { - for (appItem in appItems) { - assertTrue(appItem.waitForExists(waitingTime)) - } - } else { - for (appItem in appItems) { - assertFalse(appItem.waitForExists(waitingTimeShort)) - } - } + fun itemWithResIdAndText(resourceId: String, text: String): UiObject { + Log.i(TAG, "Looking for item with resource id: $resourceId and text: $text") + return mDevice.findObject(UiSelector().resourceId(resourceId).text(text)) } - fun assertItemContainingTextExists(vararg appItems: UiObject, exists: Boolean = true) { - for (appItem in appItems) { - if (exists) { - assertTrue(appItem.waitForExists(waitingTime)) - } else { - assertFalse(appItem.waitForExists(waitingTimeShort)) - } - } + fun itemWithResIdContainingText(resourceId: String, text: String): UiObject { + Log.i(TAG, "Looking for item with resource id: $resourceId and containing text: $text") + return mDevice.findObject(UiSelector().resourceId(resourceId).textContains(text)) } - fun assertItemWithDescriptionExists(vararg appItems: UiObject, exists: Boolean = true) { + fun assertUIObjectExists( + vararg appItems: UiObject, + exists: Boolean = true, + waitingTime: Long = TestAssetHelper.waitingTime, + ) { for (appItem in appItems) { if (exists) { - assertTrue(appItem.waitForExists(waitingTime)) + assertTrue("${appItem.selector} does not exist", appItem.waitForExists(waitingTime)) + Log.i(TAG, "assertUIObjectExists: Verified ${appItem.selector} exists") } else { - assertFalse(appItem.waitForExists(waitingTimeShort)) + assertFalse("${appItem.selector} exists", appItem.waitForExists(waitingTimeShort)) + Log.i(TAG, "assertUIObjectExists: Verified ${appItem.selector} does not exist") } } } - fun assertCheckedItemWithResIdExists(vararg appItems: UiObject) { + fun assertUIObjectIsGone(vararg appItems: UiObject) { for (appItem in appItems) { - assertTrue(appItem.waitForExists(waitingTime)) + assertTrue("${appItem.selector} is not gone", appItem.waitUntilGone(waitingTime)) + Log.i(TAG, "assertUIObjectIsGone: Verified ${appItem.selector} is gone") } } - fun assertCheckedItemWithResIdAndTextExists(vararg appItems: UiObject) { + fun assertItemTextEquals(vararg appItems: UiObject, expectedText: String, isEqual: Boolean = true) { for (appItem in appItems) { - assertTrue(appItem.waitForExists(waitingTime)) - } - } - - fun assertItemWithResIdAndDescriptionExists(vararg appItems: UiObject) { - for (appItem in appItems) { - assertTrue(appItem.waitForExists(waitingTime)) + if (isEqual) { + assertTrue( + "${appItem.selector} text does not equal to $expectedText", + appItem.text.equals(expectedText), + ) + Log.i(TAG, "assertItemTextEquals: Verified ${appItem.selector} text equals to $expectedText") + } else { + assertFalse( + "${appItem.selector} text equals to $expectedText", + appItem.text.equals(expectedText), + ) + Log.i(TAG, "assertItemTextEquals: Verified ${appItem.selector} text does not equal to $expectedText") + } } } - fun assertItemWithResIdAndTextExists(vararg appItems: UiObject, exists: Boolean = true) { + fun assertItemTextContains(vararg appItems: UiObject, itemText: String) { for (appItem in appItems) { - if (exists) { - assertTrue(appItem.waitForExists(waitingTime)) - } else { - assertFalse(appItem.waitForExists(waitingTimeShort)) - } + assertTrue( + "${appItem.selector} text does not contain $itemText", + appItem.text.contains(itemText), + ) + Log.i(TAG, "assertItemTextContains: Verified ${appItem.selector} text contains $itemText") } } fun assertItemIsEnabledAndVisible(vararg appItems: UiObject) { for (appItem in appItems) { assertTrue(appItem.waitForExists(waitingTime) && appItem.isEnabled) + Log.i(TAG, "assertItemIsEnabledAndVisible: Verified ${appItem.selector} is visible and enabled") } } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/RetryTestRule.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/RetryTestRule.kt index 89f01e6527..ad1ac26ea1 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/RetryTestRule.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/RetryTestRule.kt @@ -13,9 +13,9 @@ import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement import org.mozilla.fenix.components.PermissionStorage +import org.mozilla.fenix.helpers.AppAndSystemHelper.setNetworkEnabled import org.mozilla.fenix.helpers.IdlingResourceHelper.unregisterAllIdlingResources import org.mozilla.fenix.helpers.TestHelper.appContext -import org.mozilla.fenix.helpers.TestHelper.setNetworkEnabled /** * Rule to retry flaky tests for a given number of times, catching some of the more common exceptions. 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 af05288497..06bc94ee6f 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt @@ -2,83 +2,43 @@ * 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/. */ -@file:Suppress("DEPRECATION") - package org.mozilla.fenix.helpers -import android.Manifest -import android.app.ActivityManager -import android.app.PendingIntent -import android.content.ActivityNotFoundException -import android.content.ClipData -import android.content.ClipboardManager import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.content.res.Configuration -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Color import android.net.Uri -import android.os.Build -import android.os.storage.StorageManager -import android.os.storage.StorageVolume -import android.provider.Settings import android.util.Log import android.view.View -import androidx.annotation.RequiresApi -import androidx.browser.customtabs.CustomTabsIntent -import androidx.test.espresso.Espresso +import androidx.test.core.app.launchActivity import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.IdlingRegistry -import androidx.test.espresso.IdlingResource import androidx.test.espresso.action.ViewActions.longClick import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.intent.Intents -import androidx.test.espresso.intent.Intents.intended -import androidx.test.espresso.intent.matcher.IntentMatchers.toPackage import androidx.test.espresso.matcher.ViewMatchers.hasSibling import androidx.test.espresso.matcher.ViewMatchers.withChild import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withParent import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.rule.ActivityTestRule -import androidx.test.runner.permission.PermissionRequester import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiObject import androidx.test.uiautomator.UiScrollable import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.Until -import junit.framework.AssertionFailedError -import mozilla.components.browser.state.search.SearchEngine -import mozilla.components.browser.state.state.availableSearchEngines import mozilla.components.support.ktx.android.content.appName import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers.allOf import org.hamcrest.Matcher import org.junit.Assert -import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue -import org.mozilla.fenix.Config -import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R -import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity -import org.mozilla.fenix.ext.components -import org.mozilla.fenix.helpers.Constants.PackageName.YOUTUBE_APP +import org.mozilla.fenix.helpers.Constants.TAG import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort import org.mozilla.fenix.helpers.ext.waitNotNull -import org.mozilla.fenix.helpers.idlingresource.NetworkConnectionIdlingResource -import org.mozilla.fenix.ui.robots.BrowserRobot import org.mozilla.fenix.ui.robots.clickPageObject -import org.mozilla.fenix.utils.IntentUtils -import org.mozilla.gecko.util.ThreadUtils -import java.io.File -import java.util.Locale object TestHelper { @@ -125,14 +85,6 @@ object TestHelper { activity.launchActivity(null) } - fun getPermissionAllowID(): String { - return when - (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - true -> "com.android.permissioncontroller" - false -> "com.android.packageinstaller" - } - } - fun waitUntilObjectIsFound(resourceName: String) { mDevice.waitNotNull( Until.findObjects(By.res(resourceName)), @@ -164,180 +116,13 @@ object TestHelper { onView(withId(resId)).check(ViewAssertions.matches(withText(CoreMatchers.containsString(urlSubstring)))) } - fun openAppFromExternalLink(url: String) { - val context = InstrumentationRegistry.getInstrumentation().getTargetContext() - val intent = Intent().apply { - action = Intent.ACTION_VIEW - data = Uri.parse(url) - `package` = packageName - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - try { - context.startActivity(intent) - } catch (ex: ActivityNotFoundException) { - intent.setPackage(null) - context.startActivity(intent) - } - } - - @RequiresApi(Build.VERSION_CODES.R) - fun deleteDownloadedFileOnStorage(fileName: String) { - val storageManager: StorageManager? = appContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager? - val storageVolumes = storageManager!!.storageVolumes - val storageVolume: StorageVolume = storageVolumes[0] - val file = File(storageVolume.directory!!.path + "/Download/" + fileName) - try { - if (file.exists()) { - file.delete() - Log.d("TestLog", "File delete try 1") - assertFalse("The file was not deleted", file.exists()) - } - } catch (e: AssertionError) { - file.delete() - Log.d("TestLog", "File delete retried") - assertFalse("The file was not deleted", file.exists()) - } - } - - fun setNetworkEnabled(enabled: Boolean) { - val networkDisconnectedIdlingResource = NetworkConnectionIdlingResource(false) - val networkConnectedIdlingResource = NetworkConnectionIdlingResource(true) - - when (enabled) { - true -> { - mDevice.executeShellCommand("svc data enable") - mDevice.executeShellCommand("svc wifi enable") - - // Wait for network connection to be completely enabled - IdlingRegistry.getInstance().register(networkConnectedIdlingResource) - Espresso.onIdle { - IdlingRegistry.getInstance().unregister(networkConnectedIdlingResource) - } - } - - false -> { - mDevice.executeShellCommand("svc data disable") - mDevice.executeShellCommand("svc wifi disable") - - // Wait for network connection to be completely disabled - IdlingRegistry.getInstance().register(networkDisconnectedIdlingResource) - Espresso.onIdle { - IdlingRegistry.getInstance().unregister(networkDisconnectedIdlingResource) - } - } - } - } - - fun createCustomTabIntent( - pageUrl: String, - customMenuItemLabel: String = "", - customActionButtonDescription: String = "", - ): Intent { - val appContext = InstrumentationRegistry.getInstrumentation() - .targetContext - .applicationContext - val pendingIntent = PendingIntent.getActivity(appContext, 0, Intent(), IntentUtils.defaultIntentPendingFlags) - val customTabsIntent = CustomTabsIntent.Builder() - .addMenuItem(customMenuItemLabel, pendingIntent) - .setShareState(CustomTabsIntent.SHARE_STATE_ON) - .setActionButton( - createTestBitmap(), - customActionButtonDescription, - pendingIntent, - true, - ) - .build() - customTabsIntent.intent.data = Uri.parse(pageUrl) - return customTabsIntent.intent - } - - private fun createTestBitmap(): Bitmap { - val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bitmap) - canvas.drawColor(Color.GREEN) - return bitmap - } - - fun isPackageInstalled(packageName: String): Boolean { - return try { - val packageManager = InstrumentationRegistry.getInstrumentation().context.packageManager - packageManager.getApplicationInfo(packageName, 0).enabled - } catch (exception: PackageManager.NameNotFoundException) { - false - } - } - - fun assertExternalAppOpens(appPackageName: String) { - if (isPackageInstalled(appPackageName)) { - try { - intended(toPackage(appPackageName)) - } catch (e: AssertionFailedError) { - e.printStackTrace() - } - } else { - mDevice.waitNotNull( - Until.findObject(By.text("Could not open file")), - waitingTime, - ) - } - } - - fun assertNativeAppOpens(appPackageName: String, url: String = "") { - if (isPackageInstalled(appPackageName)) { - mDevice.waitForIdle(waitingTimeShort) - assertTrue( - mDevice.findObject(UiSelector().packageName(appPackageName)) - .waitForExists(waitingTime), - ) - } else { - BrowserRobot().verifyUrl(url) - } - } - - fun assertYoutubeAppOpens() = intended(toPackage(YOUTUBE_APP)) - - /** - * Checks whether the latest activity of the application is used for custom tabs or PWAs. - * - * @return Boolean value that helps us know if the current activity supports custom tabs or PWAs. - */ - fun isExternalAppBrowserActivityInCurrentTask(): Boolean { - val activityManager = appContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - - mDevice.waitForIdle(waitingTimeShort) - - return activityManager.appTasks[0].taskInfo.topActivity!!.className == ExternalAppBrowserActivity::class.java.name - } - - /** - * Run test with automatically registering idling resources and cleanup. - * - * @param idlingResources zero or more [IdlingResource] to be used when running [testBlock]. - * @param testBlock test code to execute. - */ - fun registerAndCleanupIdlingResources( - vararg idlingResources: IdlingResource, - testBlock: () -> Unit, - ) { - idlingResources.forEach { - IdlingRegistry.getInstance().register(it) - } - - try { - testBlock() - } finally { - idlingResources.forEach { - IdlingRegistry.getInstance().unregister(it) - } - } - } - // exit from Menus to home screen or browser fun exitMenu() { val toolbar = mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar")) while (!toolbar.waitForExists(waitingTimeShort)) { mDevice.pressBack() + Log.i(TAG, "exitMenu: Exiting app settings menus using device back button") } } @@ -356,172 +141,8 @@ object TestHelper { ) } - fun getStringResource(id: Int, argument: String = appName) = appContext.resources.getString(id, argument) - - // Permission allow dialogs differ on various Android APIs - fun grantSystemPermission() { - val whileUsingTheAppPermissionButton: UiObject = - mDevice.findObject(UiSelector().textContains("While using the app")) - - val allowPermissionButton: UiObject = - mDevice.findObject( - UiSelector() - .textContains("Allow") - .className("android.widget.Button"), - ) - - if (Build.VERSION.SDK_INT >= 23) { - if (whileUsingTheAppPermissionButton.waitForExists(waitingTimeShort)) { - whileUsingTheAppPermissionButton.click() - } else if (allowPermissionButton.waitForExists(waitingTimeShort)) { - allowPermissionButton.click() - } - } - } - - // Permission deny dialogs differ on various Android APIs - fun denyPermission() { - mDevice.findObject(UiSelector().textContains("Deny")).waitForExists(waitingTime) - mDevice.findObject(UiSelector().textContains("Deny")).click() - } - - fun isTestLab(): Boolean { - return Settings.System.getString(appContext.contentResolver, "firebase.test.lab").toBoolean() - } - - private val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') - fun generateRandomString(stringLength: Int) = - (1..stringLength) - .map { kotlin.random.Random.nextInt(0, charPool.size) } - .map(charPool::get) - .joinToString("") - - /** - * Changes the default language of the entire device, not just the app. - * Runs on Debug variant as we don't want to adjust Release permission manifests - * Runs the test in its testBlock. - * Cleans up and sets the default locale after it's done. - */ - fun runWithSystemLocaleChanged(locale: Locale, testRule: ActivityTestRule, testBlock: () -> Unit) { - if (Config.channel.isDebug) { - /* Sets permission to change device language */ - PermissionRequester().apply { - addPermissions( - Manifest.permission.CHANGE_CONFIGURATION, - ) - requestPermissions() - } - - val defaultLocale = Locale.getDefault() - - try { - setSystemLocale(locale) - testBlock() - ThreadUtils.runOnUiThread { testRule.activity.recreate() } - } catch (e: Exception) { - e.printStackTrace() - } finally { - setSystemLocale(defaultLocale) - } - } - } - - /** - * Changes the default language of the entire device, not just the app. - */ - fun setSystemLocale(locale: Locale) { - val activityManagerNative = Class.forName("android.app.ActivityManagerNative") - val am = activityManagerNative.getMethod("getDefault", *arrayOfNulls(0)) - .invoke(activityManagerNative, *arrayOfNulls(0)) - val config = InstrumentationRegistry.getInstrumentation().context.resources.configuration - config.javaClass.getDeclaredField("locale")[config] = locale - config.javaClass.getDeclaredField("userSetLocale").setBoolean(config, true) - am.javaClass.getMethod( - "updateConfiguration", - Configuration::class.java, - ).invoke(am, config) - } - - /** - * Creates clipboard data. - */ - fun setTextToClipBoard(context: Context, message: String) { - val clipBoard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clipData = ClipData.newPlainText("label", message) - - clipBoard.setPrimaryClip(clipData) - } - - /** - * Returns sponsored shortcut title based on the index. - */ - fun getSponsoredShortcutTitle(position: Int): String { - val sponsoredShortcut = mDevice.findObject( - UiSelector() - .resourceId("$packageName:id/top_site_item") - .index(position - 1), - ).getChild( - UiSelector() - .resourceId("$packageName:id/top_site_title"), - ).text - - return sponsoredShortcut - } - fun verifyLightThemeApplied(expected: Boolean) = assertFalse("Light theme not selected", expected) fun verifyDarkThemeApplied(expected: Boolean) = assertTrue("Dark theme not selected", expected) - - /** - * Wrapper for tests to run only when certain conditions are met. - * For example: this method will avoid accidentally running a test on GV versions where the feature is disabled. - */ - fun runWithCondition(condition: Boolean, testBlock: () -> Unit) { - if (condition) { - testBlock() - } - } - - fun putAppToBackground() { - mDevice.pressRecentApps() - mDevice.findObject(UiSelector().resourceId("$packageName:id/container")).waitUntilGone(waitingTime) - } - - fun bringAppToForeground() { - mDevice.pressRecentApps() - mDevice.findObject(UiSelector().resourceId("$packageName:id/container")).waitForExists(waitingTime) - } - - fun verifyKeyboardVisibility(isExpectedToBeVisible: Boolean = true) { - mDevice.waitForIdle() - - assertEquals( - "Keyboard not shown", - isExpectedToBeVisible, - mDevice - .executeShellCommand("dumpsys input_method | grep mInputShown") - .contains("mInputShown=true"), - ) - } - - /** - * The list of Search engines for the "home" region of the user. - * For en-us it will return the 6 engines selected by default: Google, Bing, DuckDuckGo, Amazon, Ebay, Wikipedia. - */ - fun getRegionSearchEnginesList(): List { - val searchEnginesList = appContext.components.core.store.state.search.regionSearchEngines - assertTrue("Search engines list returned nothing", searchEnginesList.isNotEmpty()) - return searchEnginesList - } - - /** - * The list of Search engines available to be added by user choice. - * For en-us it will return the 2 engines: Reddit, Youtube. - */ - fun getAvailableSearchEngines(): List { - val searchEnginesList = appContext.components.core.store.state.search.availableSearchEngines - assertTrue("Search engines list returned nothing", searchEnginesList.isNotEmpty()) - return searchEnginesList - } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/WaitNotNull.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/WaitNotNull.kt index 1a387fa7f4..b119e34d6b 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/WaitNotNull.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/WaitNotNull.kt @@ -4,9 +4,11 @@ package org.mozilla.fenix.helpers.ext +import android.util.Log import androidx.test.uiautomator.SearchCondition import androidx.test.uiautomator.UiDevice import org.junit.Assert.assertNotNull +import org.mozilla.fenix.helpers.Constants.TAG import org.mozilla.fenix.helpers.TestAssetHelper /** @@ -17,4 +19,8 @@ import org.mozilla.fenix.helpers.TestAssetHelper fun UiDevice.waitNotNull( searchCondition: SearchCondition<*>, waitTime: Long = TestAssetHelper.waitingTime, -) = assertNotNull(wait(searchCondition, waitTime)) +) { + Log.i(TAG, "Wait not null: $searchCondition") + assertNotNull(wait(searchCondition, waitTime)) + Log.i(TAG, "Found $searchCondition not null") +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapperTest.kt b/app/src/androidTest/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapperTest.kt index d74edfcd4b..0807c57848 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapperTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapperTest.kt @@ -4,12 +4,19 @@ package org.mozilla.fenix.onboarding.view +import io.mockk.every +import io.mockk.mockk +import mozilla.components.service.nimbus.evalJexlSafe import org.junit.Assert.assertEquals +import org.junit.Before import org.junit.Rule import org.junit.Test +import org.mozilla.experiments.nimbus.GleanPlumbMessageHelper import org.mozilla.experiments.nimbus.StringHolder import org.mozilla.fenix.R import org.mozilla.fenix.helpers.HomeActivityIntentTestRule +import org.mozilla.fenix.nimbus.FxNimbus +import org.mozilla.fenix.nimbus.JunoOnboarding import org.mozilla.fenix.nimbus.OnboardingCardData import org.mozilla.fenix.nimbus.OnboardingCardType @@ -19,28 +26,242 @@ class JunoOnboardingMapperTest { val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true) + private lateinit var junoOnboardingFeature: JunoOnboarding + private lateinit var jexlConditions: Map + private lateinit var jexlHelper: GleanPlumbMessageHelper + private lateinit var evalFunction: (String) -> Boolean + + @Before + fun setup() { + junoOnboardingFeature = FxNimbus.features.junoOnboarding.value() + jexlConditions = junoOnboardingFeature.conditions + + jexlHelper = mockk(relaxed = true) + evalFunction = { condition -> jexlHelper.evalJexlSafe(condition) } + + every { evalFunction("true") } returns true + every { evalFunction("false") } returns false + } + @Test fun showNotificationTrue_showAddWidgetFalse_pagesToDisplay_returnsSortedListOfAllConvertedPages_withoutAddWidgetPage() { val expected = listOf(defaultBrowserPageUiData, syncPageUiData, notificationPageUiData) - assertEquals(expected, unsortedAllKnownCardData.toPageUiData(true, false)) + assertEquals( + expected, + unsortedAllKnownCardData.toPageUiData( + showNotificationPage = true, + showAddWidgetPage = false, + jexlConditions = jexlConditions, + func = evalFunction, + ), + ) } @Test fun showNotificationFalse_showAddWidgetFalse_pagesToDisplay_returnsSortedListOfConvertedPages_withoutNotificationPage_and_addWidgetPage() { val expected = listOf(defaultBrowserPageUiData, syncPageUiData) - assertEquals(expected, unsortedAllKnownCardData.toPageUiData(false, false)) + assertEquals( + expected, + unsortedAllKnownCardData.toPageUiData( + showNotificationPage = false, + showAddWidgetPage = false, + jexlConditions = jexlConditions, + func = evalFunction, + ), + ) } @Test fun showNotificationFalse_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfAllConvertedPages_withoutNotificationPage() { val expected = listOf(defaultBrowserPageUiData, addSearchWidgetPageUiData, syncPageUiData) - assertEquals(expected, unsortedAllKnownCardData.toPageUiData(false, true)) + assertEquals( + expected, + unsortedAllKnownCardData.toPageUiData( + showNotificationPage = false, + showAddWidgetPage = true, + jexlConditions = jexlConditions, + func = evalFunction, + ), + ) } @Test fun showNotificationTrue_and_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfConvertedPages() { - val expected = listOf(defaultBrowserPageUiData, addSearchWidgetPageUiData, syncPageUiData, notificationPageUiData) - assertEquals(expected, unsortedAllKnownCardData.toPageUiData(true, true)) + val expected = listOf( + defaultBrowserPageUiData, + addSearchWidgetPageUiData, + syncPageUiData, + notificationPageUiData, + ) + assertEquals( + expected, + unsortedAllKnownCardData.toPageUiData( + showNotificationPage = true, + showAddWidgetPage = true, + jexlConditions = jexlConditions, + func = evalFunction, + ), + ) + } + + @Test + fun cardConditionsMatchJexlConditions_shouldDisplayCard_returnsConvertedPage() { + val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false") + val expected = listOf(defaultBrowserPageUiData) + + assertEquals( + expected, + listOf(defaultBrowserCardData).toPageUiData( + showNotificationPage = false, + showAddWidgetPage = false, + jexlConditions = jexlConditions, + func = evalFunction, + ), + ) + } + + @Test + fun noJexlConditionsAndNoCardConditions_shouldDisplayCard_returnsNoPage() { + val jexlConditions = mapOf() + val expected = emptyList() + + assertEquals( + expected, + listOf(addSearchWidgetCardDataNoConditions).toPageUiData( + showNotificationPage = false, + showAddWidgetPage = false, + jexlConditions = jexlConditions, + func = evalFunction, + ), + ) + } + + @Test + fun noJexlConditions_shouldDisplayCard_returnsNoPage() { + val jexlConditions = mapOf() + val expected = emptyList() + + assertEquals( + expected, + listOf(defaultBrowserCardData).toPageUiData( + showNotificationPage = false, + showAddWidgetPage = false, + jexlConditions = jexlConditions, + func = evalFunction, + ), + ) + } + + @Test + fun prerequisitesMatchJexlConditions_shouldDisplayCard_returnsConvertedPage() { + val jexlConditions = mapOf("ALWAYS" to "true") + val expected = listOf(defaultBrowserPageUiData) + + assertEquals( + expected, + listOf(defaultBrowserCardData).toPageUiData( + showNotificationPage = false, + showAddWidgetPage = false, + jexlConditions = jexlConditions, + func = evalFunction, + ), + ) + } + + @Test + fun prerequisitesDontMatchJexlConditions_shouldDisplayCard_returnsNoPage() { + val jexlConditions = mapOf("NEVER" to "false") + val expected = emptyList() + + assertEquals( + expected, + listOf(defaultBrowserCardData).toPageUiData( + showNotificationPage = false, + showAddWidgetPage = false, + jexlConditions = jexlConditions, + func = evalFunction, + ), + ) + } + + @Test + fun noCardConditions_shouldDisplayCard_returnsNoPage() { + val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false") + val expected = emptyList() + + assertEquals( + expected, + listOf(addSearchWidgetCardDataNoConditions).toPageUiData( + showNotificationPage = false, + showAddWidgetPage = false, + jexlConditions = jexlConditions, + func = evalFunction, + ), + ) + } + + @Test + fun noDisqualifiers_shouldDisplayCard_returnsConvertedPage() { + val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false") + val expected = listOf(defaultBrowserPageUiData) + + assertEquals( + expected, + listOf(defaultBrowserCardDataNoDisqualifiers).toPageUiData( + showNotificationPage = false, + showAddWidgetPage = false, + jexlConditions = jexlConditions, + func = evalFunction, + ), + ) + } + + @Test + fun disqualifiersMatchJexlConditions_shouldDisplayCard_returnsConvertedPage() { + val jexlConditions = mapOf("NEVER" to "false") + val expected = listOf(syncPageUiData) + + assertEquals( + expected, + listOf(syncCardData).toPageUiData( + showNotificationPage = false, + showAddWidgetPage = false, + jexlConditions = jexlConditions, + func = evalFunction, + ), + ) + } + + @Test + fun disqualifiersDontMatchJexlConditions_shouldDisplayCard_returnsNoPage() { + val jexlConditions = mapOf("NEVER" to "false") + val expected = listOf() + + assertEquals( + expected, + listOf(notificationCardData).toPageUiData( + showNotificationPage = false, + showAddWidgetPage = false, + jexlConditions = jexlConditions, + func = evalFunction, + ), + ) + } + + @Test + fun noPrerequisites_shouldDisplayCard_returnsConvertedPage() { + val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false") + val expected = listOf(syncPageUiData) + + assertEquals( + expected, + listOf(syncCardData).toPageUiData( + showNotificationPage = false, + showAddWidgetPage = false, + jexlConditions = jexlConditions, + func = evalFunction, + ), + ) } } @@ -88,7 +309,36 @@ private val defaultBrowserCardData = OnboardingCardData( primaryButtonLabel = StringHolder(null, "default browser primary button text"), secondaryButtonLabel = StringHolder(null, "default browser secondary button text"), ordering = 10, + prerequisites = listOf("ALWAYS"), + disqualifiers = listOf("NEVER"), +) + +private val defaultBrowserCardDataNoDisqualifiers = OnboardingCardData( + cardType = OnboardingCardType.DEFAULT_BROWSER, + imageRes = R.drawable.ic_onboarding_welcome, + title = StringHolder(null, "default browser title"), + body = StringHolder(null, "default browser body with link text"), + linkText = StringHolder(null, "link text"), + primaryButtonLabel = StringHolder(null, "default browser primary button text"), + secondaryButtonLabel = StringHolder(null, "default browser secondary button text"), + ordering = 10, + prerequisites = listOf("ALWAYS"), + disqualifiers = listOf(), ) + +private val addSearchWidgetCardDataNoConditions = OnboardingCardData( + cardType = OnboardingCardType.ADD_SEARCH_WIDGET, + imageRes = R.drawable.ic_onboarding_search_widget, + title = StringHolder(null, "add search widget title"), + body = StringHolder(null, "add search widget body with link text"), + linkText = StringHolder(null, "link text"), + primaryButtonLabel = StringHolder(null, "add search widget primary button text"), + secondaryButtonLabel = StringHolder(null, "add search widget secondary button text"), + ordering = 15, + prerequisites = listOf(), + disqualifiers = listOf(), +) + private val addSearchWidgetCardData = OnboardingCardData( cardType = OnboardingCardType.ADD_SEARCH_WIDGET, imageRes = R.drawable.ic_onboarding_search_widget, @@ -99,6 +349,7 @@ private val addSearchWidgetCardData = OnboardingCardData( secondaryButtonLabel = StringHolder(null, "add search widget secondary button text"), ordering = 15, ) + private val syncCardData = OnboardingCardData( cardType = OnboardingCardType.SYNC_SIGN_IN, imageRes = R.drawable.ic_onboarding_sync, @@ -107,7 +358,10 @@ private val syncCardData = OnboardingCardData( primaryButtonLabel = StringHolder(null, "sync primary button text"), secondaryButtonLabel = StringHolder(null, "sync secondary button text"), ordering = 20, + prerequisites = listOf(), + disqualifiers = listOf("NEVER"), ) + private val notificationCardData = OnboardingCardData( cardType = OnboardingCardType.NOTIFICATION_PERMISSION, imageRes = R.drawable.ic_notification_permission, @@ -116,6 +370,8 @@ private val notificationCardData = OnboardingCardData( primaryButtonLabel = StringHolder(null, "notification primary button text"), secondaryButtonLabel = StringHolder(null, "notification secondary button text"), ordering = 30, + prerequisites = listOf(), + disqualifiers = listOf("NEVER", "OTHER"), ) private val unsortedAllKnownCardData = listOf( diff --git a/app/src/androidTest/java/org/mozilla/fenix/syncintegration/SyncIntegrationTest.kt b/app/src/androidTest/java/org/mozilla/fenix/syncintegration/SyncIntegrationTest.kt index 6ebabd0850..2956ec39ee 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/syncintegration/SyncIntegrationTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/syncintegration/SyncIntegrationTest.kt @@ -26,11 +26,9 @@ import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.TestAssetHelper -import org.mozilla.fenix.helpers.ext.toUri import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.ui.robots.accountSettings import org.mozilla.fenix.ui.robots.homeScreen -import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.settingsSubMenuLoginsAndPassword @Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") @@ -55,9 +53,10 @@ class SyncIntegrationTest { mockWebServer.shutdown() } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/352905 // History item Desktop -> Fenix @Test - fun checkHistoryFromDesktopTest() { + fun syncHistoryBetweenMobileAndDesktopTest() { signInFxSync() tapReturnToPreviousApp() // Let's wait until homescreen is shown to go to three dot menu @@ -70,9 +69,10 @@ class SyncIntegrationTest { historyAfterSyncIsShown() } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/330146 // Bookmark item Desktop -> Fenix @Test - fun checkBookmarkFromDesktopTest() { + fun syncBookmarksTest() { signInFxSync() tapReturnToPreviousApp() homeScreen { @@ -81,9 +81,10 @@ class SyncIntegrationTest { bookmarkAfterSyncIsShown() } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243353 @SmokeTest @Test - fun checkAccountSettings() { + fun manageAccountSettingsTest() { signInFxSync() mDevice.waitNotNull(Until.findObjects(By.text("Account")), TestAssetHelper.waitingTime) @@ -104,9 +105,10 @@ class SyncIntegrationTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/466387 // Login item Desktop -> Fenix @Test - fun checkLoginsFromDesktopTest() { + fun synLoginsTest() { homeScreen { }.openThreeDotMenu { }.openSettings { @@ -136,44 +138,6 @@ class SyncIntegrationTest { } } - // Bookmark item Fenix -> Desktop - @Test - fun checkBookmarkFromDeviceTest() { - val defaultWebPage = "example.com".toUri()!! - navigationToolbar { - }.enterURLAndEnterToBrowser(defaultWebPage) { - }.openThreeDotMenu { - }.bookmarkPage { - }.openThreeDotMenu { - }.openSettings { - }.openTurnOnSyncMenu { - useEmailInsteadButton() - typeEmail() - tapOnContinueButton() - typePassword() - sleep(TestAssetHelper.waitingTimeShort) - tapOnSignIn() - } - } - - // History item Fenix -> Desktop - @Test - fun checkHistoryFromDeviceTest() { - val defaultWebPage = "example.com".toUri()!! - navigationToolbar { - }.enterURLAndEnterToBrowser(defaultWebPage) { - }.openThreeDotMenu { - }.openSettings { - }.openTurnOnSyncMenu { - useEmailInsteadButton() - typeEmail() - tapOnContinueButton() - typePassword() - sleep(TestAssetHelper.waitingTimeShort) - tapOnSignIn() - } - } - // Useful functions for the tests fun typeEmail() { val emailInput = mDevice.findObject( diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/AddToHomeScreenTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/AddToHomeScreenTest.kt deleted file mode 100644 index 9b1cd2143b..0000000000 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/AddToHomeScreenTest.kt +++ /dev/null @@ -1,91 +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.compose.ui.test.junit4.AndroidComposeTestRule -import okhttp3.mockwebserver.MockWebServer -import org.junit.After -import org.junit.Before -import org.junit.Ignore -import org.junit.Rule -import org.junit.Test -import org.mozilla.fenix.customannotations.SmokeTest -import org.mozilla.fenix.helpers.AndroidAssetDispatcher -import org.mozilla.fenix.helpers.HomeActivityTestRule -import org.mozilla.fenix.helpers.TestAssetHelper -import org.mozilla.fenix.helpers.TestHelper -import org.mozilla.fenix.ui.robots.browserScreen -import org.mozilla.fenix.ui.robots.homeScreen -import org.mozilla.fenix.ui.robots.searchScreen - -class AddToHomeScreenTest { - private lateinit var mockWebServer: MockWebServer - - @get:Rule - val composeTestRule = - AndroidComposeTestRule(HomeActivityTestRule.withDefaultSettingsOverrides()) { it.activity } - - @Before - fun setUp() { - mockWebServer = MockWebServer().apply { - dispatcher = AndroidAssetDispatcher() - start() - } - } - - @After - fun tearDown() { - mockWebServer.shutdown() - } - - // Verifies the Add to home screen option in a tab's 3 dot menu - @SmokeTest - @Test - fun mainMenuAddToHomeScreenTest() { - val website = TestAssetHelper.getGenericAsset(mockWebServer, 1) - val shortcutTitle = TestHelper.generateRandomString(5) - - homeScreen { - }.openNavigationToolbar { - }.enterURLAndEnterToBrowser(website.url) { - }.openThreeDotMenu { - expandMenu() - }.openAddToHomeScreen { - clickCancelShortcutButton() - } - - browserScreen { - }.openThreeDotMenu { - expandMenu() - }.openAddToHomeScreen { - verifyShortcutTextFieldTitle("Test_Page_1") - addShortcutName(shortcutTitle) - clickAddShortcutButton() - clickAddAutomaticallyButton() - }.openHomeScreenShortcut(shortcutTitle) { - verifyUrl(website.url.toString()) - verifyTabCounter("1") - } - } - - // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/414970 - @Ignore("Failure, more details at: https://bugzilla.mozilla.org/show_bug.cgi?id=1830005") - @SmokeTest - @Test - fun addPrivateBrowsingShortcutFromHomeScreenCFRTest() { - homeScreen { - }.triggerPrivateBrowsingShortcutPrompt { - verifyNoThanksPrivateBrowsingShortcutButton(composeTestRule) - verifyAddPrivateBrowsingShortcutButton(composeTestRule) - clickAddPrivateBrowsingShortcutButton(composeTestRule) - clickAddAutomaticallyButton() - }.openHomeScreenShortcut("Private ${TestHelper.appName}") {} - searchScreen { - verifySearchView() - }.dismissSearchBar { - verifyCommonMythsLink() - } - } -} diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/AddressAutofillTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/AddressAutofillTest.kt index b6244395b6..ac1af19ccc 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/AddressAutofillTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/AddressAutofillTest.kt @@ -18,6 +18,7 @@ import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestHelper.exitMenu import org.mozilla.fenix.helpers.TestHelper.packageName +import org.mozilla.fenix.ui.robots.autofillScreen import org.mozilla.fenix.ui.robots.clickPageObject import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.navigationToolbar @@ -25,6 +26,36 @@ import org.mozilla.fenix.ui.robots.navigationToolbar class AddressAutofillTest { private lateinit var mockWebServer: MockWebServer + object FirstAddressAutofillDetails { + var navigateToAutofillSettings = true + var isAddressAutofillEnabled = true + var userHasSavedAddress = false + var firstName = "Mozilla" + var middleName = "Fenix" + var lastName = "Firefox" + var streetAddress = "Harrison Street" + var city = "San Francisco" + var state = "Alaska" + var zipCode = "94105" + var country = "United States" + var phoneNumber = "555-5555" + var emailAddress = "foo@bar.com" + } + + object SecondAddressAutofillDetails { + var navigateToAutofillSettings = false + var firstName = "Android" + var middleName = "Test" + var lastName = "Name" + var streetAddress = "Fort Street" + var city = "San Jose" + var state = "Arizona" + var zipCode = "95141" + var country = "United States" + var phoneNumber = "777-7777" + var emailAddress = "fuu@bar.org" + } + @get:Rule val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides() @@ -48,22 +79,21 @@ class AddressAutofillTest { val addressFormPage = TestAssetHelper.getAddressFormAsset(mockWebServer) - homeScreen { - }.openThreeDotMenu { - }.openSettings { - }.openAutofillSubMenu { - clickAddAddressButton() + autofillScreen { fillAndSaveAddress( - "Mozilla", - "Fenix", - "Firefox", - "Harrison Street", - "San Francisco", - "Alaska", - "94105", - "United States", - "555-5555", - "foo@bar.com", + navigateToAutofillSettings = FirstAddressAutofillDetails.navigateToAutofillSettings, + isAddressAutofillEnabled = FirstAddressAutofillDetails.isAddressAutofillEnabled, + userHasSavedAddress = FirstAddressAutofillDetails.userHasSavedAddress, + firstName = FirstAddressAutofillDetails.firstName, + middleName = FirstAddressAutofillDetails.middleName, + lastName = FirstAddressAutofillDetails.lastName, + streetAddress = FirstAddressAutofillDetails.streetAddress, + city = FirstAddressAutofillDetails.city, + state = FirstAddressAutofillDetails.state, + zipCode = FirstAddressAutofillDetails.zipCode, + country = FirstAddressAutofillDetails.country, + phoneNumber = FirstAddressAutofillDetails.phoneNumber, + emailAddress = FirstAddressAutofillDetails.emailAddress, ) }.goBack { }.goBack { @@ -86,22 +116,21 @@ class AddressAutofillTest { @SmokeTest @Test fun deleteSavedAddressTest() { - homeScreen { - }.openThreeDotMenu { - }.openSettings { - }.openAutofillSubMenu { - clickAddAddressButton() + autofillScreen { fillAndSaveAddress( - "Mozilla", - "Fenix", - "Firefox", - "Harrison Street", - "San Francisco", - "Alaska", - "94105", - "United States", - "555-5555", - "foo@bar.com", + navigateToAutofillSettings = FirstAddressAutofillDetails.navigateToAutofillSettings, + isAddressAutofillEnabled = FirstAddressAutofillDetails.isAddressAutofillEnabled, + userHasSavedAddress = FirstAddressAutofillDetails.userHasSavedAddress, + firstName = FirstAddressAutofillDetails.firstName, + middleName = FirstAddressAutofillDetails.middleName, + lastName = FirstAddressAutofillDetails.lastName, + streetAddress = FirstAddressAutofillDetails.streetAddress, + city = FirstAddressAutofillDetails.city, + state = FirstAddressAutofillDetails.state, + zipCode = FirstAddressAutofillDetails.zipCode, + country = FirstAddressAutofillDetails.country, + phoneNumber = FirstAddressAutofillDetails.phoneNumber, + emailAddress = FirstAddressAutofillDetails.emailAddress, ) clickManageAddressesButton() clickSavedAddress("Mozilla") @@ -130,22 +159,21 @@ class AddressAutofillTest { // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1836841 @Test fun verifyEditAddressViewTest() { - homeScreen { - }.openThreeDotMenu { - }.openSettings { - }.openAutofillSubMenu { - clickAddAddressButton() + autofillScreen { fillAndSaveAddress( - "Mozilla", - "Fenix", - "Firefox", - "Harrison Street", - "San Francisco", - "Alaska", - "94105", - "United States", - "555-5555", - "foo@bar.com", + navigateToAutofillSettings = FirstAddressAutofillDetails.navigateToAutofillSettings, + isAddressAutofillEnabled = FirstAddressAutofillDetails.isAddressAutofillEnabled, + userHasSavedAddress = FirstAddressAutofillDetails.userHasSavedAddress, + firstName = FirstAddressAutofillDetails.firstName, + middleName = FirstAddressAutofillDetails.middleName, + lastName = FirstAddressAutofillDetails.lastName, + streetAddress = FirstAddressAutofillDetails.streetAddress, + city = FirstAddressAutofillDetails.city, + state = FirstAddressAutofillDetails.state, + zipCode = FirstAddressAutofillDetails.zipCode, + country = FirstAddressAutofillDetails.country, + phoneNumber = FirstAddressAutofillDetails.phoneNumber, + emailAddress = FirstAddressAutofillDetails.emailAddress, ) clickManageAddressesButton() clickSavedAddress("Mozilla") @@ -159,23 +187,21 @@ class AddressAutofillTest { val addressFormPage = TestAssetHelper.getAddressFormAsset(mockWebServer) - homeScreen { - }.openThreeDotMenu { - }.openSettings { - }.openAutofillSubMenu { - verifyAddressAutofillSection(true, false) - clickAddAddressButton() + autofillScreen { fillAndSaveAddress( - "Mozilla", - "Fenix", - "Firefox", - "Harrison Street", - "San Francisco", - "Alaska", - "94105", - "United States", - "555-5555", - "foo@bar.com", + navigateToAutofillSettings = FirstAddressAutofillDetails.navigateToAutofillSettings, + isAddressAutofillEnabled = FirstAddressAutofillDetails.isAddressAutofillEnabled, + userHasSavedAddress = FirstAddressAutofillDetails.userHasSavedAddress, + firstName = FirstAddressAutofillDetails.firstName, + middleName = FirstAddressAutofillDetails.middleName, + lastName = FirstAddressAutofillDetails.lastName, + streetAddress = FirstAddressAutofillDetails.streetAddress, + city = FirstAddressAutofillDetails.city, + state = FirstAddressAutofillDetails.state, + zipCode = FirstAddressAutofillDetails.zipCode, + country = FirstAddressAutofillDetails.country, + phoneNumber = FirstAddressAutofillDetails.phoneNumber, + emailAddress = FirstAddressAutofillDetails.emailAddress, ) } @@ -207,23 +233,21 @@ class AddressAutofillTest { val addressFormPage = TestAssetHelper.getAddressFormAsset(mockWebServer) - homeScreen { - }.openThreeDotMenu { - }.openSettings { - }.openAutofillSubMenu { - verifyAddressAutofillSection(true, false) - clickAddAddressButton() + autofillScreen { fillAndSaveAddress( - "Mozilla", - "Fenix", - "Firefox", - "Harrison Street", - "San Francisco", - "Alaska", - "94105", - "United States", - "555-5555", - "foo@bar.com", + navigateToAutofillSettings = FirstAddressAutofillDetails.navigateToAutofillSettings, + isAddressAutofillEnabled = FirstAddressAutofillDetails.isAddressAutofillEnabled, + userHasSavedAddress = FirstAddressAutofillDetails.userHasSavedAddress, + firstName = FirstAddressAutofillDetails.firstName, + middleName = FirstAddressAutofillDetails.middleName, + lastName = FirstAddressAutofillDetails.lastName, + streetAddress = FirstAddressAutofillDetails.streetAddress, + city = FirstAddressAutofillDetails.city, + state = FirstAddressAutofillDetails.state, + zipCode = FirstAddressAutofillDetails.zipCode, + country = FirstAddressAutofillDetails.country, + phoneNumber = FirstAddressAutofillDetails.phoneNumber, + emailAddress = FirstAddressAutofillDetails.emailAddress, ) } @@ -247,37 +271,36 @@ class AddressAutofillTest { val addressFormPage = TestAssetHelper.getAddressFormAsset(mockWebServer) - homeScreen { - }.openThreeDotMenu { - }.openSettings { - }.openAutofillSubMenu { - verifyAddressAutofillSection(true, false) - clickAddAddressButton() + autofillScreen { fillAndSaveAddress( - "Mozilla", - "Fenix", - "Firefox", - "Harrison Street", - "San Francisco", - "Alaska", - "94105", - "United States", - "555-5555", - "foo@bar.com", + navigateToAutofillSettings = FirstAddressAutofillDetails.navigateToAutofillSettings, + isAddressAutofillEnabled = FirstAddressAutofillDetails.isAddressAutofillEnabled, + userHasSavedAddress = FirstAddressAutofillDetails.userHasSavedAddress, + firstName = FirstAddressAutofillDetails.firstName, + middleName = FirstAddressAutofillDetails.middleName, + lastName = FirstAddressAutofillDetails.lastName, + streetAddress = FirstAddressAutofillDetails.streetAddress, + city = FirstAddressAutofillDetails.city, + state = FirstAddressAutofillDetails.state, + zipCode = FirstAddressAutofillDetails.zipCode, + country = FirstAddressAutofillDetails.country, + phoneNumber = FirstAddressAutofillDetails.phoneNumber, + emailAddress = FirstAddressAutofillDetails.emailAddress, ) clickManageAddressesButton() clickAddAddressButton() fillAndSaveAddress( - "Android", - "Test", - "Name", - "Fort Street", - "San Jose", - "Arizona", - "95141", - "United States", - "777-7777", - "fuu@bar.org", + navigateToAutofillSettings = SecondAddressAutofillDetails.navigateToAutofillSettings, + firstName = SecondAddressAutofillDetails.firstName, + middleName = SecondAddressAutofillDetails.middleName, + lastName = SecondAddressAutofillDetails.lastName, + streetAddress = SecondAddressAutofillDetails.streetAddress, + city = SecondAddressAutofillDetails.city, + state = SecondAddressAutofillDetails.state, + zipCode = SecondAddressAutofillDetails.zipCode, + country = SecondAddressAutofillDetails.country, + phoneNumber = SecondAddressAutofillDetails.phoneNumber, + emailAddress = SecondAddressAutofillDetails.emailAddress, ) verifyManageAddressesToolbarTitle() } @@ -311,37 +334,36 @@ class AddressAutofillTest { // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1836850 @Test fun verifySavedAddressCanBeEditedTest() { - homeScreen { - }.openThreeDotMenu { - }.openSettings { - }.openAutofillSubMenu { - verifyAddressAutofillSection(true, false) - clickAddAddressButton() + autofillScreen { fillAndSaveAddress( - "Mozilla", - "Fenix", - "Firefox", - "Harrison Street", - "San Francisco", - "Alaska", - "94105", - "United States", - "555-5555", - "foo@bar.com", + navigateToAutofillSettings = FirstAddressAutofillDetails.navigateToAutofillSettings, + isAddressAutofillEnabled = FirstAddressAutofillDetails.isAddressAutofillEnabled, + userHasSavedAddress = FirstAddressAutofillDetails.userHasSavedAddress, + firstName = FirstAddressAutofillDetails.firstName, + middleName = FirstAddressAutofillDetails.middleName, + lastName = FirstAddressAutofillDetails.lastName, + streetAddress = FirstAddressAutofillDetails.streetAddress, + city = FirstAddressAutofillDetails.city, + state = FirstAddressAutofillDetails.state, + zipCode = FirstAddressAutofillDetails.zipCode, + country = FirstAddressAutofillDetails.country, + phoneNumber = FirstAddressAutofillDetails.phoneNumber, + emailAddress = FirstAddressAutofillDetails.emailAddress, ) clickManageAddressesButton() clickSavedAddress("Mozilla") fillAndSaveAddress( - "Android", - "Test", - "Name", - "Fort Street", - "San Jose", - "Arizona", - "95141", - "United States", - "777-7777", - "fuu@bar.org", + navigateToAutofillSettings = SecondAddressAutofillDetails.navigateToAutofillSettings, + firstName = SecondAddressAutofillDetails.firstName, + middleName = SecondAddressAutofillDetails.middleName, + lastName = SecondAddressAutofillDetails.lastName, + streetAddress = SecondAddressAutofillDetails.streetAddress, + city = SecondAddressAutofillDetails.city, + state = SecondAddressAutofillDetails.state, + zipCode = SecondAddressAutofillDetails.zipCode, + country = SecondAddressAutofillDetails.country, + phoneNumber = SecondAddressAutofillDetails.phoneNumber, + emailAddress = SecondAddressAutofillDetails.emailAddress, ) verifyManageAddressesToolbarTitle() } @@ -370,22 +392,21 @@ class AddressAutofillTest { val addressFormPage = TestAssetHelper.getAddressFormAsset(mockWebServer) - homeScreen { - }.openThreeDotMenu { - }.openSettings { - }.openAutofillSubMenu { - clickAddAddressButton() + autofillScreen { fillAndSaveAddress( - "Mozilla", - "Fenix", - "Firefox", - "Harrison Street", - "San Francisco", - "Alaska", - "94105", - "United States", - "555-5555", - "foo@bar.com", + navigateToAutofillSettings = FirstAddressAutofillDetails.navigateToAutofillSettings, + isAddressAutofillEnabled = FirstAddressAutofillDetails.isAddressAutofillEnabled, + userHasSavedAddress = FirstAddressAutofillDetails.userHasSavedAddress, + firstName = FirstAddressAutofillDetails.firstName, + middleName = FirstAddressAutofillDetails.middleName, + lastName = FirstAddressAutofillDetails.lastName, + streetAddress = FirstAddressAutofillDetails.streetAddress, + city = FirstAddressAutofillDetails.city, + state = FirstAddressAutofillDetails.state, + zipCode = FirstAddressAutofillDetails.zipCode, + country = FirstAddressAutofillDetails.country, + phoneNumber = FirstAddressAutofillDetails.phoneNumber, + emailAddress = FirstAddressAutofillDetails.emailAddress, ) } @@ -410,23 +431,21 @@ class AddressAutofillTest { // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1836838 @Test fun verifyAutofillAddressSectionTest() { - homeScreen { - }.openThreeDotMenu { - }.openSettings { - }.openAutofillSubMenu { - verifyAddressAutofillSection(true, false) - clickAddAddressButton() + autofillScreen { fillAndSaveAddress( - "Mozilla", - "Fenix", - "Firefox", - "Harrison Street", - "San Francisco", - "Alaska", - "94105", - "United States", - "555-5555", - "foo@bar.com", + navigateToAutofillSettings = FirstAddressAutofillDetails.navigateToAutofillSettings, + isAddressAutofillEnabled = FirstAddressAutofillDetails.isAddressAutofillEnabled, + userHasSavedAddress = FirstAddressAutofillDetails.userHasSavedAddress, + firstName = FirstAddressAutofillDetails.firstName, + middleName = FirstAddressAutofillDetails.middleName, + lastName = FirstAddressAutofillDetails.lastName, + streetAddress = FirstAddressAutofillDetails.streetAddress, + city = FirstAddressAutofillDetails.city, + state = FirstAddressAutofillDetails.state, + zipCode = FirstAddressAutofillDetails.zipCode, + country = FirstAddressAutofillDetails.country, + phoneNumber = FirstAddressAutofillDetails.phoneNumber, + emailAddress = FirstAddressAutofillDetails.emailAddress, ) verifyAddressAutofillSection(true, true) clickManageAddressesButton() diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt index 5771bf96d7..e99e0fc12b 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt @@ -22,6 +22,7 @@ import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.ext.bookmarkStorage import org.mozilla.fenix.ext.settings import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.MockBrowserDataHelper.createBookmarkItem import org.mozilla.fenix.helpers.RecyclerViewIdlingResource @@ -30,7 +31,6 @@ import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestHelper.appContext import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton import org.mozilla.fenix.helpers.TestHelper.longTapSelectItem -import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources import org.mozilla.fenix.helpers.TestHelper.restartApp import org.mozilla.fenix.ui.robots.bookmarksMenu import org.mozilla.fenix.ui.robots.browserScreen diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/BrowsingErrorPagesTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/BrowsingErrorPagesTest.kt index f8249e8fe3..de05a516a6 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/BrowsingErrorPagesTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/BrowsingErrorPagesTest.kt @@ -13,12 +13,12 @@ import org.junit.Test import org.mozilla.fenix.R import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.setNetworkEnabled +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.RetryTestRule import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset -import org.mozilla.fenix.helpers.TestHelper.getStringResource -import org.mozilla.fenix.helpers.TestHelper.setNetworkEnabled import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.clickPageObject import org.mozilla.fenix.ui.robots.navigationToolbar diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeBookmarksTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeBookmarksTest.kt index e4fe0e4a82..dbfd9afda1 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeBookmarksTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeBookmarksTest.kt @@ -20,6 +20,7 @@ import org.mozilla.fenix.R import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.ext.bookmarkStorage import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.RecyclerViewIdlingResource import org.mozilla.fenix.helpers.RetryTestRule @@ -27,7 +28,6 @@ import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton import org.mozilla.fenix.helpers.TestHelper.exitMenu import org.mozilla.fenix.helpers.TestHelper.longTapSelectItem -import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources import org.mozilla.fenix.ui.robots.bookmarksMenu import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.homeScreen @@ -78,6 +78,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/522919 @Test fun verifyEmptyBookmarksMenuTest() { homeScreen { @@ -101,6 +102,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/522920 @Test fun cancelCreateBookmarkFolderTest() { homeScreen { @@ -114,6 +116,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2299619 @Test fun cancelingChangesInEditModeAreNotSavedTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -138,6 +141,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325633 @SmokeTest @Test fun editBookmarksNameAndUrlTest() { @@ -165,6 +169,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/341696 @Test fun copyBookmarkURLTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -191,8 +196,9 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325634 @Test - fun threeDotMenuShareBookmarkTest() { + fun shareBookmarkTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) browserScreen { @@ -211,6 +217,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325636 @Test fun openBookmarkInNewTabTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -229,6 +236,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1919261 @Test fun verifyOpenAllInNewTabsOptionTest() { val webPages = listOf( @@ -271,6 +279,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1919262 @Test fun verifyOpenAllInPrivateTabsTest() { val webPages = listOf( @@ -306,6 +315,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325637 @Test fun openBookmarkInPrivateTabTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -324,6 +334,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325635 @Test fun deleteBookmarkTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -351,6 +362,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2300275 @Test fun bookmarksMultiSelectionToolbarItemsTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -376,6 +388,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2300276 @SmokeTest @Test fun openMultipleSelectedBookmarksInANewTabTest() { @@ -406,6 +419,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2300277 @Test fun openMultipleSelectedBookmarksInPrivateTabTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -429,6 +443,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325644 @SmokeTest @Test fun deleteMultipleSelectedBookmarksTest() { @@ -476,8 +491,9 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2301355 @Test - fun multipleSelectionShareButtonTest() { + fun shareMultipleSelectedBookmarksTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) browserScreen { @@ -500,6 +516,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325639 @Test fun createBookmarkFolderTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -524,6 +541,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325645 @Test fun navigateBookmarksFoldersTest() { homeScreen { @@ -547,8 +565,9 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/374855 @Test - fun cantSelectDesktopFoldersTest() { + fun cantSelectDefaultFoldersTest() { homeScreen { }.openThreeDotMenu { }.openBookmarks { @@ -561,6 +580,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2299703 @Test fun deleteBookmarkInEditModeTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -583,6 +603,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715710 @Test fun verifySearchBookmarksViewTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -654,6 +675,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715711 @Test fun verifyVoiceSearchInBookmarksTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -707,6 +729,7 @@ class ComposeBookmarksTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/325642 // Verifies that deleting a Bookmarks folder also removes the item from inside it. @SmokeTest @Test diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeCollectionTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeCollectionTest.kt index 82aca81f02..990b02c892 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeCollectionTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeCollectionTest.kt @@ -65,6 +65,7 @@ class ComposeCollectionTest { mockWebServer.shutdown() } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/353823 @SmokeTest @Test fun createFirstCollectionUsingHomeScreenButtonTest() { @@ -97,6 +98,7 @@ class ComposeCollectionTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343422 @SmokeTest @Test fun verifyExpandedCollectionItemsTest() { @@ -147,6 +149,7 @@ class ComposeCollectionTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343425 @SmokeTest @Test fun openAllTabsFromACollectionTest() { @@ -181,6 +184,7 @@ class ComposeCollectionTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343426 @SmokeTest @Test fun shareAllTabsFromACollectionTest() { @@ -208,6 +212,7 @@ class ComposeCollectionTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343428 // Test running on beta/release builds in CI: // caution when making changes to it, so they don't block the builds @SmokeTest @@ -248,6 +253,7 @@ class ComposeCollectionTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2319453 // open a webpage, and add currently opened tab to existing collection @Test fun saveTabToExistingCollectionFromMainMenuTest() { @@ -276,8 +282,9 @@ class ComposeCollectionTest { } } + // Testrail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343423 @Test - fun verifyAddTabButtonOfCollectionMenu() { + fun saveTabToExistingCollectionUsingTheAddTabButtonTest() { val firstWebPage = getGenericAsset(mockWebServer, 1) val secondWebPage = getGenericAsset(mockWebServer, 2) @@ -303,6 +310,7 @@ class ComposeCollectionTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343424 @Test fun renameCollectionTest() { val webPage = getGenericAsset(mockWebServer, 1) @@ -325,6 +333,7 @@ class ComposeCollectionTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/991248 @Test fun createCollectionUsingSelectTabsButtonTest() { val firstWebPage = getGenericAsset(mockWebServer, 1) @@ -347,6 +356,7 @@ class ComposeCollectionTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2319455 @Test fun removeTabFromCollectionUsingTheCloseButtonTest() { val webPage = getGenericAsset(mockWebServer, 1) @@ -378,6 +388,7 @@ class ComposeCollectionTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/343427 @Test fun removeTabFromCollectionUsingSwipeLeftActionTest() { val testPage = getGenericAsset(mockWebServer, 1) @@ -410,6 +421,7 @@ class ComposeCollectionTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/991278 @Test fun removeTabFromCollectionUsingSwipeRightActionTest() { val testPage = getGenericAsset(mockWebServer, 1) @@ -442,6 +454,7 @@ class ComposeCollectionTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/991276 @Test fun createCollectionByLongPressingOpenTabsTest() { val firstWebPage = getGenericAsset(mockWebServer, 1) @@ -474,6 +487,7 @@ class ComposeCollectionTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/344897 @Test fun navigateBackInCollectionFlowTest() { val webPage = getGenericAsset(mockWebServer, 1) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeContextMenusTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeContextMenusTest.kt index 4883a5ebb1..85a3e0c4ce 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeContextMenusTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeContextMenusTest.kt @@ -15,13 +15,13 @@ import org.junit.Rule import org.junit.Test import org.mozilla.fenix.ext.settings import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.assertExternalAppOpens import org.mozilla.fenix.helpers.Constants import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithText import org.mozilla.fenix.helpers.RetryTestRule import org.mozilla.fenix.helpers.TestAssetHelper -import org.mozilla.fenix.helpers.TestHelper.assertExternalAppOpens import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton import org.mozilla.fenix.ui.robots.clickContextMenuItem import org.mozilla.fenix.ui.robots.clickPageObject @@ -75,6 +75,7 @@ class ComposeContextMenusTest { mockWebServer.shutdown() } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243837 @Test fun verifyOpenLinkNewTabContextMenuOptionTest() { val pageLinks = @@ -98,6 +99,7 @@ class ComposeContextMenusTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/244655 @Test fun verifyOpenLinkInNewPrivateTabContextMenuOptionTest() { val pageLinks = @@ -120,6 +122,7 @@ class ComposeContextMenusTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243832 @Test fun verifyCopyLinkContextMenuOptionTest() { val pageLinks = @@ -140,6 +143,7 @@ class ComposeContextMenusTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243838 @Test fun verifyShareLinkContextMenuOptionTest() { val pageLinks = @@ -159,6 +163,7 @@ class ComposeContextMenusTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243833 @Test fun verifyOpenImageNewTabContextMenuOptionTest() { val pageLinks = @@ -178,6 +183,7 @@ class ComposeContextMenusTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243834 @Test fun verifyCopyImageLocationContextMenuOptionTest() { val pageLinks = @@ -198,6 +204,7 @@ class ComposeContextMenusTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243835 @Test fun verifySaveImageContextMenuOptionTest() { val pageLinks = @@ -221,6 +228,7 @@ class ComposeContextMenusTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/352050 @Test fun verifyContextMenuLinkVariationsTest() { val pageLinks = @@ -244,6 +252,7 @@ class ComposeContextMenusTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2333840 @Test fun verifyPDFContextMenuLinkVariationsTest() { val genericURL = @@ -264,6 +273,7 @@ class ComposeContextMenusTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/832094 @Test fun verifyOpenLinkInAppContextMenuOptionTest() { val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeHistoryTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeHistoryTest.kt index 8ea850b708..5a2fed714d 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeHistoryTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeHistoryTest.kt @@ -22,12 +22,12 @@ import org.mozilla.fenix.R import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.ext.settings import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.RecyclerViewIdlingResource import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestHelper.exitMenu import org.mozilla.fenix.helpers.TestHelper.longTapSelectItem -import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.historyMenu import org.mozilla.fenix.ui.robots.homeScreen @@ -74,6 +74,7 @@ class ComposeHistoryTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243285 @Test fun verifyEmptyHistoryMenuTest() { homeScreen { @@ -85,6 +86,7 @@ class ComposeHistoryTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2302742 // Test running on beta/release builds in CI: // caution when making changes to it, so they don't block the builds @SmokeTest @@ -110,6 +112,7 @@ class ComposeHistoryTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243288 @Test fun deleteHistoryItemTest() { val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -130,6 +133,7 @@ class ComposeHistoryTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1848881 @SmokeTest @Test fun deleteAllHistoryTest() { @@ -154,6 +158,7 @@ class ComposeHistoryTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/339690 @SmokeTest @Test fun historyMultiSelectionToolbarItemsTest() { @@ -182,6 +187,7 @@ class ComposeHistoryTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/339696 @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807268") @Test fun openMultipleSelectedHistoryItemsInANewTabTest() { @@ -212,6 +218,7 @@ class ComposeHistoryTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/346098 @Test fun openMultipleSelectedHistoryItemsInPrivateTabTest() { val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -237,6 +244,7 @@ class ComposeHistoryTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/346099 @Test fun deleteMultipleSelectedHistoryItemsTest() { val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -271,6 +279,7 @@ class ComposeHistoryTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/339701 @Test fun shareMultipleSelectedHistoryItemsTest() { val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -297,6 +306,7 @@ class ComposeHistoryTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715627 @Test fun verifySearchHistoryViewTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -339,6 +349,7 @@ class ComposeHistoryTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715631 @Test fun verifyVoiceSearchInHistoryTest() { homeScreen { @@ -351,6 +362,7 @@ class ComposeHistoryTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715632 @Test fun verifySearchForHistoryItemsTest() { val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -376,6 +388,7 @@ class ComposeHistoryTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1715634 @Test fun verifyDeletedHistoryItemsCanNotBeSearchedTest() { val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -418,6 +431,7 @@ class ComposeHistoryTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903590 // Test running on beta/release builds in CI: // caution when making changes to it, so they don't block the builds @SmokeTest diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeHomeScreenTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeHomeScreenTest.kt index fab9595237..a7d9f7ae03 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeHomeScreenTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeHomeScreenTest.kt @@ -146,22 +146,6 @@ class ComposeHomeScreenTest { } } - // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1569867 - @Test - fun verifyJumpBackInContextualHintTest() { - activityTestRule.activityRule.applySettingsExceptions { - it.isJumpBackInCFREnabled = true - } - - val genericPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) - - navigationToolbar { - }.enterURLAndEnterToBrowser(genericPage.url) { - }.goToHomescreen { - verifyJumpBackInMessage(activityTestRule) - } - } - // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1569839 @Test fun verifyCustomizeHomepageButtonTest() { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeMediaNotificationTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeMediaNotificationTest.kt index db762cf693..63c0ad07b6 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeMediaNotificationTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeMediaNotificationTest.kt @@ -139,7 +139,7 @@ class ComposeMediaNotificationTest { mDevice.pressBack() } - // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903595 + // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903595 @Test fun mediaSystemNotificationInPrivateModeTest() { val audioTestPage = TestAssetHelper.getAudioPageAsset(mockWebServer) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeNavigationToolbarTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeNavigationToolbarTest.kt index 35a912596d..d5cee7ed02 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeNavigationToolbarTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeNavigationToolbarTest.kt @@ -14,9 +14,9 @@ import org.junit.Rule import org.junit.Test import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.TestAssetHelper -import org.mozilla.fenix.helpers.TestHelper.runWithSystemLocaleChanged import org.mozilla.fenix.ui.robots.navigationToolbar import java.util.Locale @@ -56,6 +56,7 @@ class ComposeNavigationToolbarTest { mockWebServer.shutdown() } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/987326 // Swipes the nav bar left/right to switch between tabs @SmokeTest @Test @@ -75,6 +76,7 @@ class ComposeNavigationToolbarTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/987327 // Because it requires changing system prefs, this test will run only on Debug builds @Test fun swipeToSwitchTabInRTLTest() { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSearchTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSearchTest.kt index eaba11fa23..71391b5228 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSearchTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSearchTest.kt @@ -17,6 +17,10 @@ import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.mozilla.fenix.customannotations.SmokeTest +import org.mozilla.fenix.helpers.AppAndSystemHelper.assertNativeAppOpens +import org.mozilla.fenix.helpers.AppAndSystemHelper.denyPermission +import org.mozilla.fenix.helpers.AppAndSystemHelper.grantSystemPermission +import org.mozilla.fenix.helpers.AppAndSystemHelper.verifyKeyboardVisibility import org.mozilla.fenix.helpers.Constants import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.MatcherHelper @@ -27,7 +31,6 @@ import org.mozilla.fenix.helpers.SearchDispatcher import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestHelper import org.mozilla.fenix.helpers.TestHelper.exitMenu -import org.mozilla.fenix.helpers.TestHelper.verifyKeyboardVisibility import org.mozilla.fenix.ui.robots.clickContextMenuItem import org.mozilla.fenix.ui.robots.clickPageObject import org.mozilla.fenix.ui.robots.homeScreen @@ -61,7 +64,6 @@ class ComposeSearchTest { isRecentTabsFeatureEnabled = false, isTCPCFREnabled = false, isWallpaperOnboardingEnabled = false, - isCookieBannerReductionDialogEnabled = false, tabsTrayRewriteEnabled = true, ), ) { it.activity } @@ -79,13 +81,14 @@ class ComposeSearchTest { searchMockServer.shutdown() } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154189 @Test fun verifySearchBarItemsTest() { navigationToolbar { verifyDefaultSearchEngine("Google") verifySearchBarPlaceholder("Search or enter address") }.clickUrlbar { - TestHelper.verifyKeyboardVisibility(isExpectedToBeVisible = true) + verifyKeyboardVisibility(isExpectedToBeVisible = true) verifyScanButtonVisibility(visible = true) verifyVoiceSearchButtonVisibility(enabled = true) verifySearchBarPlaceholder("Search or enter address") @@ -95,6 +98,7 @@ class ComposeSearchTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154190 @Test fun verifySearchSelectorMenuItemsTest() { homeScreen { @@ -109,6 +113,7 @@ class ComposeSearchTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154194 @Test fun verifySearchPlaceholderForGeneralDefaultSearchEnginesTest() { generalEnginesList.forEach { @@ -126,6 +131,7 @@ class ComposeSearchTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154195 @Test fun verifySearchPlaceholderForNotDefaultGeneralSearchEnginesTest() { val generalEnginesList = listOf("DuckDuckGo", "Bing") @@ -140,6 +146,7 @@ class ComposeSearchTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154196 @Test fun verifySearchPlaceholderForTopicSpecificSearchEnginesTest() { val topicEnginesList = listOf("Amazon.com", "Wikipedia", "eBay") @@ -154,6 +161,7 @@ class ComposeSearchTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1059459 @SmokeTest @Test fun verifyQRScanningCameraAccessDialogTest() { @@ -163,7 +171,7 @@ class ComposeSearchTest { homeScreen { }.openSearch { clickScanButton() - TestHelper.denyPermission() + denyPermission() clickScanButton() clickDismissPermissionRequiredDialog() } @@ -171,10 +179,11 @@ class ComposeSearchTest { }.openSearch { clickScanButton() clickGoToPermissionsSettings() - TestHelper.assertNativeAppOpens(Constants.PackageName.ANDROID_SETTINGS) + assertNativeAppOpens(Constants.PackageName.ANDROID_SETTINGS) } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/235397 @SmokeTest @Test fun scanQRCodeToOpenAWebpageTest() { @@ -184,11 +193,12 @@ class ComposeSearchTest { homeScreen { }.openSearch { clickScanButton() - TestHelper.grantSystemPermission() + grantSystemPermission() verifyScannerOpen() } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154191 @Test fun verifyScanButtonAvailableOnlyForGeneralSearchEnginesTest() { generalEnginesList.forEach { @@ -210,6 +220,7 @@ class ComposeSearchTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/235395 // Verifies a temporary change of search engine from the Search shortcut menu @SmokeTest @Test @@ -229,6 +240,7 @@ class ComposeSearchTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/233589 @Test fun defaultSearchEnginesCanBeSetFromSearchSelectorMenuTest() { searchScreen { @@ -246,6 +258,7 @@ class ComposeSearchTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/522918 @Test fun verifyClearSearchButtonTest() { homeScreen { @@ -256,6 +269,7 @@ class ComposeSearchTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1623441 @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704") @SmokeTest @Test @@ -284,6 +298,7 @@ class ComposeSearchTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1592229 @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704") @Test fun verifyAPageIsAddedToASearchGroupOnlyOnceTest() { @@ -329,6 +344,7 @@ class ComposeSearchTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1591782 @Ignore("Failing due to known bug, see https://github.com/mozilla-mobile/fenix/issues/23818") @Test fun searchGroupIsGeneratedWhenNavigatingInTheSameTabTest() { @@ -352,6 +368,7 @@ class ComposeSearchTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1591781 @SmokeTest @Test fun searchGroupIsNotGeneratedForLinksOpenedInPrivateTabsTest() { @@ -383,6 +400,7 @@ class ComposeSearchTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1592269 @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704") @SmokeTest @Test @@ -425,6 +443,7 @@ class ComposeSearchTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1592242 @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704") @Test fun deleteSearchGroupFromHomeScreenTest() { @@ -465,6 +484,7 @@ class ComposeSearchTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1592235 @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704") @Test fun openAPageFromHomeScreenSearchGroupTest() { @@ -513,6 +533,7 @@ class ComposeSearchTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1592238 @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704") @Test fun shareAPageFromHomeScreenSearchGroupTest() { @@ -551,6 +572,7 @@ class ComposeSearchTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1232633 // Default search code for Google-US @Test fun defaultSearchCodeGoogleUS() { @@ -571,6 +593,7 @@ class ComposeSearchTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1232637 // Default search code for Bing-US @Test fun defaultSearchCodeBingUS() { @@ -600,6 +623,7 @@ class ComposeSearchTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1232638 // Default search code for DuckDuckGo-US @Test fun defaultSearchCodeDuckDuckGoUS() { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSettingsDeleteBrowsingDataOnQuitTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSettingsDeleteBrowsingDataOnQuitTest.kt index 4ed0adadbd..6994cd4504 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSettingsDeleteBrowsingDataOnQuitTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSettingsDeleteBrowsingDataOnQuitTest.kt @@ -17,17 +17,18 @@ import org.junit.Test import org.mozilla.fenix.R import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.clearDownloadsFolder +import org.mozilla.fenix.helpers.AppAndSystemHelper.setNetworkEnabled +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.MatcherHelper import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper.getStorageTestAsset -import org.mozilla.fenix.helpers.TestHelper -import org.mozilla.fenix.helpers.TestHelper.deleteDownloadedFileOnStorage import org.mozilla.fenix.helpers.TestHelper.exitMenu import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.restartApp -import org.mozilla.fenix.helpers.TestHelper.setNetworkEnabled import org.mozilla.fenix.ui.robots.clickPageObject +import org.mozilla.fenix.ui.robots.downloadRobot import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.navigationToolbar @@ -65,8 +66,12 @@ class ComposeSettingsDeleteBrowsingDataOnQuitTest { @After fun tearDown() { mockWebServer.shutdown() + + // Check and clear the downloads folder + clearDownloadsFolder() } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416048 @Test fun deleteBrowsingDataOnQuitSettingTest() { homeScreen { @@ -95,6 +100,7 @@ class ComposeSettingsDeleteBrowsingDataOnQuitTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416049 @Test fun deleteOpenTabsOnQuitTest() { val testPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -119,6 +125,7 @@ class ComposeSettingsDeleteBrowsingDataOnQuitTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416050 @Test fun deleteBrowsingHistoryOnQuitTest() { val genericPage = @@ -182,6 +189,7 @@ class ComposeSettingsDeleteBrowsingDataOnQuitTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1243096 @SmokeTest @Test fun deleteDownloadsOnQuitTest() { @@ -194,13 +202,10 @@ class ComposeSettingsDeleteBrowsingDataOnQuitTest { clickDeleteBrowsingOnQuitButtonSwitch() exitMenu() } - navigationToolbar { - }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { - }.clickDownloadLink("smallZip.zip") { - verifyDownloadPrompt("smallZip.zip") - }.clickDownload { + downloadRobot { + openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "smallZip.zip") verifyDownloadCompleteNotificationPopup() - }.closeCompletedDownloadPrompt { + }.closeDownloadPrompt { }.goToHomescreen { }.openThreeDotMenu { clickQuit() @@ -212,9 +217,9 @@ class ComposeSettingsDeleteBrowsingDataOnQuitTest { }.openDownloadsManager { verifyEmptyDownloadsList() } - deleteDownloadedFileOnStorage("smallZip.zip") } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416053 @SmokeTest @Test fun deleteSitePermissionsOnQuitTest() { @@ -250,9 +255,10 @@ class ComposeSettingsDeleteBrowsingDataOnQuitTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416052 @Test fun deleteCachedFilesOnQuitTest() { - val pocketTopArticles = TestHelper.getStringResource(R.string.pocket_pinned_top_articles) + val pocketTopArticles = getStringResource(R.string.pocket_pinned_top_articles) homeScreen { }.openThreeDotMenu { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSettingsDeleteBrowsingDataTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSettingsDeleteBrowsingDataTest.kt index 7b092bbb51..6235c8f8fc 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSettingsDeleteBrowsingDataTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeSettingsDeleteBrowsingDataTest.kt @@ -13,15 +13,15 @@ import org.junit.Test import org.mozilla.fenix.R import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.setNetworkEnabled +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper.getStorageTestAsset import org.mozilla.fenix.helpers.TestHelper.exitMenu -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.restartApp -import org.mozilla.fenix.helpers.TestHelper.setNetworkEnabled import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.clickPageObject import org.mozilla.fenix.ui.robots.homeScreen @@ -58,6 +58,7 @@ class ComposeSettingsDeleteBrowsingDataTest { mockWebServer.shutdown() } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/937561 @Test fun deleteBrowsingDataOptionStatesTest() { homeScreen { @@ -116,6 +117,7 @@ class ComposeSettingsDeleteBrowsingDataTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/517811 @Test fun deleteOpenTabsBrowsingDataWithNoOpenTabsTest() { homeScreen { @@ -133,6 +135,7 @@ class ComposeSettingsDeleteBrowsingDataTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/353531 @SmokeTest @Test fun deleteOpenTabsBrowsingDataTest() { @@ -165,6 +168,7 @@ class ComposeSettingsDeleteBrowsingDataTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/378864 @SmokeTest @Test fun deleteBrowsingHistoryTest() { @@ -195,6 +199,7 @@ class ComposeSettingsDeleteBrowsingDataTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416041 @SmokeTest @Test fun deleteCookiesAndSiteDataTest() { @@ -233,6 +238,7 @@ class ComposeSettingsDeleteBrowsingDataTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416042 @SmokeTest @Test fun deleteCachedFilesTest() { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeTabbedBrowsingTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeTabbedBrowsingTest.kt index f1f5da3455..0137600492 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeTabbedBrowsingTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeTabbedBrowsingTest.kt @@ -25,7 +25,6 @@ import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton import org.mozilla.fenix.helpers.TestHelper.closeApp import org.mozilla.fenix.helpers.TestHelper.restartApp -import org.mozilla.fenix.helpers.TestHelper.verifyKeyboardVisibility import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.clickPageObject @@ -86,7 +85,7 @@ class ComposeTabbedBrowsingTest { mockWebServer.shutdown() } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903599 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903599 @Test fun closeAllTabsTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -118,9 +117,9 @@ class ComposeTabbedBrowsingTest { } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903604 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2349580 @Test - fun closingTabsMethodsTest() { + fun closingTabsTest() { val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1) navigationToolbar { @@ -133,36 +132,43 @@ class ComposeTabbedBrowsingTest { } browserScreen { verifyTabCounter("1") - }.openComposeTabDrawer(composeTestRule) { - closeTab() } - homeScreen { - verifyTabCounter("0") - }.openNavigationToolbar { + } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903604 + @Test + fun swipeToCloseTabsTest() { + val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1) + + navigationToolbar { }.enterURLAndEnterToBrowser(genericURL.url) { + waitForPageToLoad() }.openComposeTabDrawer(composeTestRule) { verifyExistingOpenTabs("Test_Page_1") swipeTabRight("Test_Page_1") + verifySnackBarText("Tab closed") } homeScreen { verifyTabCounter("0") }.openNavigationToolbar { }.enterURLAndEnterToBrowser(genericURL.url) { + waitForPageToLoad() }.openComposeTabDrawer(composeTestRule) { verifyExistingOpenTabs("Test_Page_1") swipeTabLeft("Test_Page_1") + verifySnackBarText("Tab closed") } homeScreen { verifyTabCounter("0") } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903591 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903591 @Test - fun closingPrivateTabsMethodsTest() { + fun closingPrivateTabsTest() { val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1) - homeScreen { }.togglePrivateBrowsingMode() + homeScreen { }.togglePrivateBrowsingMode(switchPBModeOn = true) navigationToolbar { }.enterURLAndEnterToBrowser(genericURL.url) { }.openComposeTabDrawer(composeTestRule) { @@ -173,33 +179,10 @@ class ComposeTabbedBrowsingTest { } browserScreen { verifyTabCounter("1") - }.openComposeTabDrawer(composeTestRule) { - closeTab() - } - homeScreen { - verifyTabCounter("0") - }.openNavigationToolbar { - }.enterURLAndEnterToBrowser(genericURL.url) { - }.openComposeTabDrawer(composeTestRule) { - verifyExistingOpenTabs("Test_Page_1") - swipeTabRight("Test_Page_1") - verifySnackBarText("Private tab closed") - } - homeScreen { - verifyTabCounter("0") - }.openNavigationToolbar { - }.enterURLAndEnterToBrowser(genericURL.url) { - }.openComposeTabDrawer(composeTestRule) { - verifyExistingOpenTabs("Test_Page_1") - swipeTabLeft("Test_Page_1") - verifySnackBarText("Private tab closed") - } - homeScreen { - verifyTabCounter("0") } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903606 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903606 @SmokeTest @Test fun tabMediaControlButtonTest() { @@ -240,6 +223,7 @@ class ComposeTabbedBrowsingTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903602 @Test fun verifyTabTrayNotShowingStateHalfExpanded() { homeScreen { @@ -263,7 +247,7 @@ class ComposeTabbedBrowsingTest { } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903600 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903600 @Test fun verifyEmptyTabTray() { homeScreen { @@ -280,7 +264,7 @@ class ComposeTabbedBrowsingTest { } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903585 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903585 @Test fun verifyEmptyPrivateTabsTrayTest() { homeScreen { @@ -298,7 +282,7 @@ class ComposeTabbedBrowsingTest { } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903601 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903601 @Test fun verifyTabsTrayWithOpenTabTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -322,7 +306,7 @@ class ComposeTabbedBrowsingTest { } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903587 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903587 @SmokeTest @Test fun verifyPrivateTabsTrayWithOpenTabTest() { @@ -347,78 +331,133 @@ class ComposeTabbedBrowsingTest { } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/927314 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/927314 @Test - fun tabsCounterShortcutMenuTest() { - val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + fun tabsCounterShortcutMenuCloseTabTest() { + val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2) navigationToolbar { - }.enterURLAndEnterToBrowser(defaultWebPage.url) {} + }.enterURLAndEnterToBrowser(firstWebPage.url) { + waitForPageToLoad() + }.goToHomescreen { + }.openNavigationToolbar { + }.enterURLAndEnterToBrowser(secondWebPage.url) { + waitForPageToLoad() + } navigationToolbar { }.openTabButtonShortcutsMenu { verifyTabButtonShortcutMenuItems() }.closeTabFromShortcutsMenu { + browserScreen { + verifyTabCounter("1") + verifyPageContent(firstWebPage.content) + } + } + } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2343663 + @Test + fun tabsCounterShortcutMenuNewPrivateTabTest() { + val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + + navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) {} navigationToolbar { }.openTabButtonShortcutsMenu { }.openNewPrivateTabFromShortcutsMenu { - verifyKeyboardVisibility() verifySearchBarPlaceholder("Search or enter address") - // dismiss search dialog }.dismissSearchBar { - verifyPrivateBrowsingHomeScreenItems() + verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = true) } + } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2343662 + @Test + fun tabsCounterShortcutMenuNewTabTest() { + val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) {} navigationToolbar { }.openTabButtonShortcutsMenu { }.openNewTabFromShortcutsMenu { - verifyKeyboardVisibility() verifySearchBarPlaceholder("Search or enter address") - // dismiss search dialog }.dismissSearchBar { - verifyHomeWordmark() - verifyNavigationToolbar() + verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = false) } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/927314 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/927315 @Test - fun privateTabsCounterShortcutMenuTest() { - val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + fun privateTabsCounterShortcutMenuCloseTabTest() { + val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2) - homeScreen {}.togglePrivateBrowsingMode() + homeScreen {}.togglePrivateBrowsingMode(switchPBModeOn = true) navigationToolbar { - }.enterURLAndEnterToBrowser(defaultWebPage.url) {} + }.enterURLAndEnterToBrowser(firstWebPage.url) { + waitForPageToLoad() + }.goToHomescreen { + }.openNavigationToolbar { + }.enterURLAndEnterToBrowser(secondWebPage.url) { + waitForPageToLoad() + } navigationToolbar { }.openTabButtonShortcutsMenu { verifyTabButtonShortcutMenuItems() }.closeTabFromShortcutsMenu { - }.enterURLAndEnterToBrowser(defaultWebPage.url) {} + browserScreen { + verifyTabCounter("1") + verifyPageContent(firstWebPage.content) + } + }.openTabButtonShortcutsMenu { + }.closeTabFromShortcutsMenu { + homeScreen { + verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = true) + } + } + } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2344199 + @Test + fun privateTabsCounterShortcutMenuNewPrivateTabTest() { + val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + + homeScreen {}.togglePrivateBrowsingMode(switchPBModeOn = true) + navigationToolbar { + }.enterURLAndEnterToBrowser(defaultWebPage.url) { + waitForPageToLoad() + } navigationToolbar { }.openTabButtonShortcutsMenu { }.openNewPrivateTabFromShortcutsMenu { - verifyKeyboardVisibility() verifySearchBarPlaceholder("Search or enter address") - // dismiss search dialog }.dismissSearchBar { - verifyCommonMythsLink() + verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = true) } + } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2344198 + @Test + fun privateTabsCounterShortcutMenuNewTabTest() { + val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + + homeScreen {}.togglePrivateBrowsingMode(switchPBModeOn = true) navigationToolbar { - }.enterURLAndEnterToBrowser(defaultWebPage.url) {} + }.enterURLAndEnterToBrowser(defaultWebPage.url) { + verifyPageContent(defaultWebPage.content) + } navigationToolbar { }.openTabButtonShortcutsMenu { }.openNewTabFromShortcutsMenu { - verifyKeyboardVisibility() - verifySearchBarPlaceholder("Search or enter address") - // dismiss search dialog + verifySearchToolbar(isDisplayed = true) }.dismissSearchBar { - // Verify normal browsing homescreen - verifyExistingTopSitesList() + verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = false) } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/1046683 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1046683 @Test fun verifySyncedTabsWhenUserIsNotSignedInTest() { navigationToolbar { @@ -432,7 +471,7 @@ class ComposeTabbedBrowsingTest { } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903598 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903598 @SmokeTest @Test fun shareTabsFromTabsTrayTest() { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeTopSitesTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeTopSitesTest.kt index 830d81b589..5168ef1c7d 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeTopSitesTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeTopSitesTest.kt @@ -15,11 +15,11 @@ import org.junit.Test import org.mozilla.fenix.R import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.DataGenerationHelper.generateRandomString +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton -import org.mozilla.fenix.helpers.TestHelper.generateRandomString -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.waitUntilSnackbarGone import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.homeScreenWithComposeTopSites @@ -60,6 +60,7 @@ class ComposeTopSitesTest { mockWebServer.shutdown() } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/532598 @SmokeTest @Test fun addAWebsiteAsATopSiteTest() { @@ -78,6 +79,7 @@ class ComposeTopSitesTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/532599 @Test fun openTopSiteInANewTabTest() { val defaultWebPage = getGenericAsset(mockWebServer, 1) @@ -105,6 +107,7 @@ class ComposeTopSitesTest { mDevice.pressBack() } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/532600 @Test fun openTopSiteInANewPrivateTabTest() { val defaultWebPage = getGenericAsset(mockWebServer, 1) @@ -126,6 +129,7 @@ class ComposeTopSitesTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1110321 @Test fun renameATopSiteTest() { val defaultWebPage = getGenericAsset(mockWebServer, 1) @@ -150,6 +154,7 @@ class ComposeTopSitesTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/532601 @Test fun removeTopSiteUsingMenuButtonTest() { val defaultWebPage = getGenericAsset(mockWebServer, 1) @@ -176,6 +181,7 @@ class ComposeTopSitesTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2323641 @Test fun removeTopSiteFromMainMenuTest() { val defaultWebPage = getGenericAsset(mockWebServer, 1) @@ -199,6 +205,7 @@ class ComposeTopSitesTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/561582 // Expected for en-us defaults @Test fun verifyENLocalesDefaultTopSitesListTest() { @@ -211,6 +218,7 @@ class ComposeTopSitesTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1050642 @SmokeTest @Test fun addAndRemoveMostViewedTopSiteTest() { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt index 8546e1fe62..4e95fba11b 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt @@ -14,13 +14,13 @@ import org.junit.Rule import org.junit.Test import org.mozilla.fenix.ext.settings import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.assertExternalAppOpens import org.mozilla.fenix.helpers.Constants.PackageName.YOUTUBE_APP import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithText import org.mozilla.fenix.helpers.RetryTestRule import org.mozilla.fenix.helpers.TestAssetHelper -import org.mozilla.fenix.helpers.TestHelper.assertExternalAppOpens import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton import org.mozilla.fenix.ui.robots.clickContextMenuItem import org.mozilla.fenix.ui.robots.clickPageObject diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/CookieBannerReductionTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/CookieBannerReductionTest.kt deleted file mode 100644 index 72176cea7c..0000000000 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/CookieBannerReductionTest.kt +++ /dev/null @@ -1,72 +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.core.net.toUri -import org.junit.Ignore -import org.junit.Rule -import org.junit.Test -import org.mozilla.fenix.customannotations.SmokeTest -import org.mozilla.fenix.helpers.HomeActivityIntentTestRule -import org.mozilla.fenix.helpers.TestHelper.exitMenu -import org.mozilla.fenix.helpers.TestHelper.restartApp -import org.mozilla.fenix.ui.robots.browserScreen -import org.mozilla.fenix.ui.robots.homeScreen - -class CookieBannerReductionTest { - @get:Rule - val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true) - - // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1980504 - // Bug causing flakiness https://bugzilla.mozilla.org/show_bug.cgi?id=1807440 - @Ignore("Disabled: https://bugzilla.mozilla.org/show_bug.cgi?id=1852803") - @SmokeTest - @Test - fun verifyCookieBannerReductionFunctionalityTest() { - val webSite = "startsiden.no" - - homeScreen { - }.openNavigationToolbar { - }.enterURLAndEnterToBrowser(webSite.toUri()) { - waitForPageToLoad() - verifyCookieBannerExists(exists = true) - }.openThreeDotMenu { - }.openSettings { - verifySettingsOptionSummary("Cookie banner reduction", "Off") - }.openCookieBannerReductionSubMenu { - verifyCookieBannerView(isCookieBannerReductionChecked = false) - clickCookieBannerReductionToggle() - verifyCheckedCookieBannerReductionToggle(isCookieBannerReductionChecked = true) - }.goBack { - verifySettingsOptionSummary("Cookie banner reduction", "On") - } - - exitMenu() - - browserScreen { - verifyCookieBannerExists(exists = false) - } - - restartApp(activityTestRule) - - browserScreen { - verifyCookieBannerExists(exists = false) - }.openThreeDotMenu { - }.openSettings { - }.openCookieBannerReductionSubMenu { - clickCookieBannerReductionToggle() - verifyCheckedCookieBannerReductionToggle(false) - } - - exitMenu() - - browserScreen { - waitForPageToLoad() - }.openThreeDotMenu { - }.refreshPage { - verifyCookieBannerExists(exists = false) - } - } -} diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/CrashReportingTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/CrashReportingTest.kt index c47eb8156b..f246930bd4 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/CrashReportingTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/CrashReportingTest.kt @@ -16,10 +16,10 @@ import org.junit.Test import org.mozilla.fenix.R import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.TestAssetHelper -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.ui.robots.clickPageObject import org.mozilla.fenix.ui.robots.homeScreen diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/CreditCardAutofillTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/CreditCardAutofillTest.kt index e01687e687..88a78ed614 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/CreditCardAutofillTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/CreditCardAutofillTest.kt @@ -12,14 +12,14 @@ import org.junit.Rule import org.junit.Test import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.bringAppToForeground +import org.mozilla.fenix.helpers.AppAndSystemHelper.putAppToBackground import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText import org.mozilla.fenix.helpers.TestAssetHelper -import org.mozilla.fenix.helpers.TestHelper.bringAppToForeground import org.mozilla.fenix.helpers.TestHelper.exitMenu import org.mozilla.fenix.helpers.TestHelper.packageName -import org.mozilla.fenix.helpers.TestHelper.putAppToBackground import org.mozilla.fenix.ui.robots.clickPageObject import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.navigationToolbar @@ -369,6 +369,7 @@ class CreditCardAutofillTest { } // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1512794 + @Ignore("Failing, see https://bugzilla.mozilla.org/show_bug.cgi?id=1853625") @Test fun verifyMultipleCreditCardsCanBeAddedTest() { val creditCardFormPage = TestAssetHelper.getCreditCardFormAsset(mockWebServer) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/CustomTabsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/CustomTabsTest.kt index 014fd09ace..c7832211dd 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/CustomTabsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/CustomTabsTest.kt @@ -19,14 +19,14 @@ import org.junit.Test import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.openAppFromExternalLink +import org.mozilla.fenix.helpers.DataGenerationHelper.createCustomTabIntent import org.mozilla.fenix.helpers.FeatureSettingsHelperDelegate import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText import org.mozilla.fenix.helpers.MatcherHelper.itemWithText import org.mozilla.fenix.helpers.TestAssetHelper -import org.mozilla.fenix.helpers.TestHelper.createCustomTabIntent import org.mozilla.fenix.helpers.TestHelper.exitMenu -import org.mozilla.fenix.helpers.TestHelper.openAppFromExternalLink import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.clickPageObject import org.mozilla.fenix.ui.robots.customTabScreen diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadFileTypesTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadFileTypesTest.kt index 547d1b2ba5..ff0a744ade 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadFileTypesTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadFileTypesTest.kt @@ -5,13 +5,15 @@ package org.mozilla.fenix.ui import androidx.core.net.toUri +import org.junit.After import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.mozilla.fenix.customannotations.SmokeTest +import org.mozilla.fenix.helpers.AppAndSystemHelper.clearDownloadsFolder import org.mozilla.fenix.helpers.HomeActivityIntentTestRule -import org.mozilla.fenix.ui.robots.navigationToolbar +import org.mozilla.fenix.ui.robots.downloadRobot /** * Test for verifying downloading a list of different file types: @@ -28,6 +30,12 @@ class DownloadFileTypesTest(fileName: String) { @get:Rule val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides() + @After + fun tearDown() { + // Check and clear the downloads folder + clearDownloadsFolder() + } + companion object { // Creating test data. The test will take each file name as a parameter and run it individually. @JvmStatic @@ -45,17 +53,14 @@ class DownloadFileTypesTest(fileName: String) { ) } - // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/251028&group_by=cases:section_id&group_id=31659&group_order=asc + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/251028 @SmokeTest @Test fun allFilesAppearInDownloadsMenuTest() { - navigationToolbar { - }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { - }.clickDownloadLink(downloadFile) { - verifyDownloadPrompt(downloadFile) - }.clickDownload { + downloadRobot { + openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = downloadFile) verifyDownloadCompleteNotificationPopup() - }.closeCompletedDownloadPrompt { + }.closeDownloadPrompt { }.openThreeDotMenu { }.openDownloadsManager { waitForDownloadsListToExist() diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt index c0d544261f..d49e99dd34 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt @@ -13,17 +13,18 @@ import org.junit.Rule import org.junit.Test import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.assertExternalAppOpens +import org.mozilla.fenix.helpers.AppAndSystemHelper.clearDownloadsFolder +import org.mozilla.fenix.helpers.AppAndSystemHelper.deleteDownloadedFileOnStorage +import org.mozilla.fenix.helpers.AppAndSystemHelper.setNetworkEnabled import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_APPS_PHOTOS import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_DOCS import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.MatcherHelper.itemWithText import org.mozilla.fenix.helpers.TestAssetHelper -import org.mozilla.fenix.helpers.TestHelper.assertExternalAppOpens import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton -import org.mozilla.fenix.helpers.TestHelper.deleteDownloadedFileOnStorage import org.mozilla.fenix.helpers.TestHelper.exitMenu import org.mozilla.fenix.helpers.TestHelper.mDevice -import org.mozilla.fenix.helpers.TestHelper.setNetworkEnabled import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.clickPageObject import org.mozilla.fenix.ui.robots.downloadRobot @@ -72,63 +73,45 @@ class DownloadTest { mockWebServer.shutdown() setNetworkEnabled(enabled = true) + + // Check and clear the downloads folder + clearDownloadsFolder() } // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243844 @Test fun verifyTheDownloadPromptsTest() { - downloadFile = "web_icon.png" - - navigationToolbar { - }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { - waitForPageToLoad() - }.clickDownloadLink(downloadFile) { - verifyDownloadPrompt(downloadFile) - }.clickDownload { + downloadRobot { + openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "web_icon.png") verifyDownloadCompleteNotificationPopup() }.clickOpen("image/png") {} downloadRobot { verifyPhotosAppOpens() } - mDevice.pressBack() - deleteDownloadedFileOnStorage(downloadFile) } // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2299405 @Test fun verifyTheDownloadFailedNotificationsTest() { - downloadFile = "1GB.zip" - - navigationToolbar { - }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { - waitForPageToLoad() - }.clickDownloadLink(downloadFile) { - verifyDownloadPrompt(downloadFile) - }.clickDownload { + downloadRobot { + openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "1GB.zip") setNetworkEnabled(enabled = false) - verifyDownloadFailedPrompt(downloadFile) + verifyDownloadFailedPrompt("1GB.zip") setNetworkEnabled(enabled = true) clickTryAgainButton() } mDevice.openNotification() notificationShade { verifySystemNotificationDoesNotExist("Download failed") - verifySystemNotificationExists(downloadFile) + verifySystemNotificationExists("1GB.zip") }.closeNotificationTray {} - deleteDownloadedFileOnStorage(downloadFile) } // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2298616 @Test fun verifyDownloadCompleteNotificationTest() { - downloadFile = "web_icon.png" - - navigationToolbar { - }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { - waitForPageToLoad() - }.clickDownloadLink(downloadFile) { - verifyDownloadPrompt(downloadFile) - }.clickDownload { + downloadRobot { + openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "web_icon.png") verifyDownloadCompleteNotificationPopup() } mDevice.openNotification() @@ -146,7 +129,6 @@ class DownloadTest { ) verifySystemNotificationDoesNotExist("Firefox Fenix") }.closeNotificationTray {} - deleteDownloadedFileOnStorage(downloadFile) } // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/451563 @@ -154,19 +136,9 @@ class DownloadTest { @SmokeTest @Test fun pauseResumeCancelDownloadTest() { - // Clear the "Firefox Fenix default browser notification" - notificationShade { - cancelAllShownNotifications() + downloadRobot { + openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "1GB.zip") } - - downloadFile = "1GB.zip" - - navigationToolbar { - }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { - waitForPageToLoad() - }.clickDownloadLink(downloadFile) { - verifyDownloadPrompt(downloadFile) - }.clickDownload {} mDevice.openNotification() notificationShade { verifySystemNotificationExists("Firefox Fenix") @@ -175,7 +147,7 @@ class DownloadTest { verifySystemNotificationExists("Download paused") clickDownloadNotificationControlButton("RESUME") clickDownloadNotificationControlButton("CANCEL") - verifySystemNotificationDoesNotExist(downloadFile) + verifySystemNotificationDoesNotExist("1GB.zip") mDevice.pressBack() } browserScreen { @@ -183,79 +155,41 @@ class DownloadTest { }.openDownloadsManager { verifyEmptyDownloadsList() } - deleteDownloadedFileOnStorage(downloadFile) } // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2301474 @Test fun openDownloadedFileFromDownloadsMenuTest() { - downloadFile = "web_icon.png" - - navigationToolbar { - }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { - waitForPageToLoad() - }.clickDownloadLink(downloadFile) { - verifyDownloadPrompt(downloadFile) - }.clickDownload { + downloadRobot { + openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "web_icon.png") verifyDownloadCompleteNotificationPopup() } browserScreen { }.openThreeDotMenu { }.openDownloadsManager { - verifyDownloadedFileName(downloadFile) - openDownloadedFile(downloadFile) + verifyDownloadedFileName("web_icon.png") + openDownloadedFile("web_icon.png") verifyPhotosAppOpens() mDevice.pressBack() } - deleteDownloadedFileOnStorage(downloadFile) - } - - // Save PDF file from the share overlay - @SmokeTest - @Test - fun saveAndOpenPdfTest() { - val genericURL = - TestAssetHelper.getGenericAsset(mockWebServer, 3) - downloadFile = "pdfForm.pdf" - - navigationToolbar { - }.enterURLAndEnterToBrowser(genericURL.url) { - clickPageObject(itemWithText("PDF form file")) - }.openThreeDotMenu { - }.clickShareButton { - }.clickSaveAsPDF { - verifyDownloadPrompt(downloadFile) - }.clickDownload { - }.clickOpen("application/pdf") { - assertExternalAppOpens(GOOGLE_DOCS) - } - deleteDownloadedFileOnStorage(downloadFile) } // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1114970 @Test fun deleteDownloadedFileTest() { - downloadFile = "smallZip.zip" - - navigationToolbar { - }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { - waitForPageToLoad() - }.clickDownloadLink(downloadFile) { - verifyDownloadPrompt(downloadFile) - }.clickDownload { - verifyDownloadedFileName(downloadFile) + downloadRobot { + openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "smallZip.zip") } browserScreen { }.openThreeDotMenu { }.openDownloadsManager { - verifyDownloadedFileName(downloadFile) - deleteDownloadedItem(downloadFile) + verifyDownloadedFileName("smallZip.zip") + deleteDownloadedItem("smallZip.zip") clickSnackbarButton("UNDO") - verifyDownloadedFileName(downloadFile) - deleteDownloadedItem(downloadFile) + verifyDownloadedFileName("smallZip.zip") + deleteDownloadedItem("smallZip.zip") verifyEmptyDownloadsList() } - deleteDownloadedFileOnStorage(downloadFile) } // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2302662 @@ -264,14 +198,10 @@ class DownloadTest { val firstDownloadedFile = "smallZip.zip" val secondDownloadedFile = "textfile.txt" - navigationToolbar { - }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { - waitForPageToLoad() - }.clickDownloadLink(firstDownloadedFile) { - verifyDownloadPrompt(firstDownloadedFile) - }.clickDownload { + downloadRobot { + openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = firstDownloadedFile) verifyDownloadedFileName(firstDownloadedFile) - }.closeCompletedDownloadPrompt { + }.closeDownloadPrompt { }.clickDownloadLink(secondDownloadedFile) { verifyDownloadPrompt(secondDownloadedFile) }.clickDownload { @@ -295,29 +225,21 @@ class DownloadTest { clickMultiSelectRemoveButton() verifyEmptyDownloadsList() } - deleteDownloadedFileOnStorage(firstDownloadedFile) - deleteDownloadedFileOnStorage(secondDownloadedFile) } // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2301537 @Test fun fileDeletedFromStorageIsDeletedEverywhereTest() { - val downloadFile = "smallZip.zip" - - navigationToolbar { - }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { - waitForPageToLoad() - }.clickDownloadLink(downloadFile) { - verifyDownloadPrompt(downloadFile) - }.clickDownload { + downloadRobot { + openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "smallZip.zip") verifyDownloadCompleteNotificationPopup() } browserScreen { }.openThreeDotMenu { }.openDownloadsManager { waitForDownloadsListToExist() - verifyDownloadedFileName(downloadFile) - deleteDownloadedFileOnStorage(downloadFile) + verifyDownloadedFileName("smallZip.zip") + deleteDownloadedFileOnStorage("smallZip.zip") }.exitDownloadsManagerToBrowser { }.openThreeDotMenu { }.openDownloadsManager { @@ -325,39 +247,24 @@ class DownloadTest { exitMenu() } - navigationToolbar { - }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { - waitForPageToLoad() - }.clickDownloadLink(downloadFile) { - verifyDownloadPrompt(downloadFile) - }.clickDownload { + downloadRobot { + openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "smallZip.zip") verifyDownloadCompleteNotificationPopup() } browserScreen { }.openThreeDotMenu { }.openDownloadsManager { waitForDownloadsListToExist() - verifyDownloadedFileName(downloadFile) + verifyDownloadedFileName("smallZip.zip") } - deleteDownloadedFileOnStorage(downloadFile) } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/457112 @Ignore("Failing: https://bugzilla.mozilla.org/show_bug.cgi?id=1840994") @Test fun systemNotificationCantBeDismissedWhileInProgressTest() { - // Clear the "Firefox Fenix default browser notification" - notificationShade { - cancelAllShownNotifications() - } - - downloadFile = "1GB.zip" - - navigationToolbar { - }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { - waitForPageToLoad() - }.clickDownloadLink(downloadFile) { - verifyDownloadPrompt(downloadFile) - }.clickDownload { + downloadRobot { + openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "1GB.zip") } browserScreen { }.openNotificationShade { @@ -368,31 +275,21 @@ class DownloadTest { swipeDownloadNotification(direction = "Right", shouldDismissNotification = false) clickDownloadNotificationControlButton("CANCEL") } - deleteDownloadedFileOnStorage(downloadFile) } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2299297 @Test fun notificationCanBeDismissedIfDownloadIsInterruptedTest() { - // Clear the "Firefox Fenix default browser notification" - notificationShade { - cancelAllShownNotifications() + downloadRobot { + openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "1GB.zip") } - downloadFile = "1GB.zip" - - navigationToolbar { - }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { - waitForPageToLoad() - }.clickDownloadLink(downloadFile) { - verifyDownloadPrompt(downloadFile) - }.clickDownload {} - setNetworkEnabled(enabled = false) browserScreen { }.openNotificationShade { - verifySystemNotificationExists("Download failed") expandNotificationMessage() + verifySystemNotificationExists("Download failed") swipeDownloadNotification("Left", true) verifySystemNotificationDoesNotExist("Firefox Fenix") }.closeNotificationTray {} @@ -401,26 +298,16 @@ class DownloadTest { }.closeDownloadPrompt { verifyDownloadPromptIsDismissed() } - deleteDownloadedFileOnStorage(downloadFile) } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1632384 @Test fun warningWhenClosingPrivateTabsWhileDownloadingTest() { - downloadFile = "1GB.zip" - - // Clear the "Firefox Fenix default browser notification" - notificationShade { - cancelAllShownNotifications() - } - homeScreen { }.togglePrivateBrowsingMode() - navigationToolbar { - }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { - waitForPageToLoad() - }.clickDownloadLink(downloadFile) { - verifyDownloadPrompt(downloadFile) - }.clickDownload {} + downloadRobot { + openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "1GB.zip") + } browserScreen { }.openTabDrawer { closeTab() @@ -431,28 +318,16 @@ class DownloadTest { }.openNotificationShade { verifySystemNotificationExists("Firefox Fenix") } - deleteDownloadedFileOnStorage(downloadFile) } // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2302663 @Test fun cancelActivePrivateBrowsingDownloadsTest() { - downloadFile = "1GB.zip" - - // Clear the "Firefox Fenix default browser notification" - notificationShade { - cancelAllShownNotifications() - } - homeScreen { }.togglePrivateBrowsingMode() - - navigationToolbar { - }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { - waitForPageToLoad() - }.clickDownloadLink(downloadFile) { - verifyDownloadPrompt(downloadFile) - }.clickDownload {} + downloadRobot { + openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "1GB.zip") + } browserScreen { }.openTabDrawer { closeTab() @@ -463,12 +338,13 @@ class DownloadTest { }.openNotificationShade { verifySystemNotificationDoesNotExist("Firefox Fenix") } - deleteDownloadedFileOnStorage(downloadFile) } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2048448 // Save edited PDF file from the share overlay + @SmokeTest @Test - fun saveEditedPdfTest() { + fun saveAsPdfFunctionalityTest() { val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 3) downloadFile = "pdfForm.pdf" @@ -486,6 +362,29 @@ class DownloadTest { }.clickOpen("application/pdf") { assertExternalAppOpens(GOOGLE_DOCS) } - deleteDownloadedFileOnStorage(downloadFile) + } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/244125 + @Test + fun restartDownloadFromAppNotificationAfterConnectionIsInterruptedTest() { + downloadFile = "1GB.zip" + + navigationToolbar { + }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { + waitForPageToLoad() + }.clickDownloadLink(downloadFile) { + verifyDownloadPrompt(downloadFile) + setNetworkEnabled(false) + }.clickDownload { + verifyDownloadFailedPrompt(downloadFile) + setNetworkEnabled(true) + clickTryAgainButton() + } + browserScreen { + }.openNotificationShade { + verifySystemNotificationExists("Firefox Fenix") + expandNotificationMessage() + clickDownloadNotificationControlButton("CANCEL") + } } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/EnhancedTrackingProtectionTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/EnhancedTrackingProtectionTest.kt index 2f3963caf1..168ac36c47 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/EnhancedTrackingProtectionTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/EnhancedTrackingProtectionTest.kt @@ -351,8 +351,6 @@ class EnhancedTrackingProtectionTest { enhancedTrackingProtection { }.openEnhancedTrackingProtectionSheet { }.openDetails { - verifyCrossSiteCookiesBlocked(true) - navigateBackToDetails() verifyCryptominersBlocked(true) navigateBackToDetails() verifyFingerprintersBlocked(true) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt index 8c43a56cc8..ec76f44198 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt @@ -22,13 +22,13 @@ import org.mozilla.fenix.R import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.ext.settings import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.MockBrowserDataHelper import org.mozilla.fenix.helpers.RecyclerViewIdlingResource import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestHelper.exitMenu import org.mozilla.fenix.helpers.TestHelper.longTapSelectItem -import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.historyMenu import org.mozilla.fenix.ui.robots.homeScreen @@ -466,4 +466,18 @@ class HistoryTest { verifyEmptyHistoryView() } } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/243287 + @Test + fun openHistoryItemTest() { + val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + + navigationToolbar { + }.enterURLAndEnterToBrowser(defaultWebPage.url) { + }.openThreeDotMenu { + }.openHistory { + }.openWebsite(defaultWebPage.url) { + verifyUrl(defaultWebPage.url.toString()) + } + } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt index 172886e2bb..d4d12c7b7a 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt @@ -10,6 +10,7 @@ import androidx.test.uiautomator.UiDevice import okhttp3.mockwebserver.MockWebServer import org.junit.After import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.mozilla.fenix.customannotations.SmokeTest @@ -17,8 +18,10 @@ import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.RetryTestRule import org.mozilla.fenix.helpers.TestAssetHelper +import org.mozilla.fenix.helpers.TestHelper import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.navigationToolbar +import org.mozilla.fenix.ui.robots.searchScreen /** * Tests for verifying the presence of home screen and first-run homescreen elements @@ -166,4 +169,39 @@ class HomeScreenTest { verifyCustomizeHomepageButton(true) } } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/414970 + @Ignore("Failure, more details at: https://bugzilla.mozilla.org/show_bug.cgi?id=1830005") + @SmokeTest + @Test + fun addPrivateBrowsingShortcutFromHomeScreenCFRTest() { + homeScreen { + }.triggerPrivateBrowsingShortcutPrompt { + verifyNoThanksPrivateBrowsingShortcutButton(activityTestRule) + verifyAddPrivateBrowsingShortcutButton(activityTestRule) + clickAddPrivateBrowsingShortcutButton(activityTestRule) + clickAddAutomaticallyButton() + }.openHomeScreenShortcut("Private ${TestHelper.appName}") {} + searchScreen { + verifySearchView() + }.dismissSearchBar { + verifyCommonMythsLink() + } + } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1569867 + @Test + fun verifyJumpBackInContextualHintTest() { + activityTestRule.activityRule.applySettingsExceptions { + it.isJumpBackInCFREnabled = true + } + + val genericPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + + navigationToolbar { + }.enterURLAndEnterToBrowser(genericPage.url) { + }.goToHomescreen { + verifyJumpBackInMessage(activityTestRule) + } + } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/MainMenuTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/MainMenuTest.kt index 17c589e732..97e0d5c574 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/MainMenuTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/MainMenuTest.kt @@ -16,12 +16,15 @@ import org.junit.Test import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.ext.components import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.assertNativeAppOpens +import org.mozilla.fenix.helpers.AppAndSystemHelper.assertYoutubeAppOpens +import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithCondition +import org.mozilla.fenix.helpers.Constants.PackageName.PRINT_SPOOLER +import org.mozilla.fenix.helpers.DataGenerationHelper.generateRandomString import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.MatcherHelper import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestHelper -import org.mozilla.fenix.helpers.TestHelper.assertYoutubeAppOpens -import org.mozilla.fenix.helpers.TestHelper.runWithCondition import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.clickContextMenuItem import org.mozilla.fenix.ui.robots.clickPageObject @@ -260,7 +263,7 @@ class MainMenuTest { @Test fun addPageShortcutToHomeScreenTest() { val website = TestAssetHelper.getGenericAsset(mockWebServer, 1) - val shortcutTitle = TestHelper.generateRandomString(5) + val shortcutTitle = generateRandomString(5) homeScreen { }.openNavigationToolbar { @@ -351,4 +354,33 @@ class MainMenuTest { verifyPageContent("REFRESHED") } } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2282411 + @Test + fun printWebPageFromMainMenuTest() { + val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + + navigationToolbar { + }.enterURLAndEnterToBrowser(defaultWebPage.url) { + mDevice.waitForIdle() + }.openThreeDotMenu { + }.clickPrintButton { + assertNativeAppOpens(PRINT_SPOOLER) + } + } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2282408 + @Test + fun printWebPageFromShareMenuTest() { + val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + + navigationToolbar { + }.enterURLAndEnterToBrowser(defaultWebPage.url) { + mDevice.waitForIdle() + }.openThreeDotMenu { + }.clickShareButton { + }.clickPrintButton { + assertNativeAppOpens(PRINT_SPOOLER) + } + } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ModifierTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ModifierTest.kt new file mode 100644 index 0000000000..667cea647a --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ModifierTest.kt @@ -0,0 +1,119 @@ +package org.mozilla.fenix.ui + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performScrollToIndex +import androidx.compose.ui.unit.dp +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.mozilla.fenix.compose.ext.onShown + +private const val ON_SHOWN_ROOT_TAG = "onShownRoot" +private const val ON_SHOWN_SETTLE_TIME_MS = 1000 +private const val ON_SHOWN_INDEX = 15 +private const val ON_SHOWN_NODE_COUNT = 30 + +class ModifierTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun verifyModifierOnShownWhenScrolledToWithNoSettleTime() { + var onShown = false + composeTestRule.setContent { + ModifierOnShownContent( + settleTime = 0, + onVisible = { + onShown = true + }, + ) + } + + composeTestRule.scrollToOnShownIndex() + + assertTrue(onShown) + } + + @Test + fun verifyModifierOnShownAfterSettled() { + var onShown = false + composeTestRule.setContent { + ModifierOnShownContent( + onVisible = { + onShown = true + }, + ) + } + + composeTestRule.scrollToOnShownIndex() + + assertFalse(onShown) + + composeTestRule.waitUntil(ON_SHOWN_SETTLE_TIME_MS + 500L) { onShown } + + assertTrue(onShown) + } + + @Test + fun verifyModifierOnShownWhenNotVisible() { + val indexToValidate = ON_SHOWN_NODE_COUNT - 1 + var onShown = false + composeTestRule.setContent { + ModifierOnShownContent( + indexToValidate = indexToValidate, + settleTime = 0, + onVisible = { + onShown = true + }, + ) + } + + assertFalse(onShown) + } + + private fun ComposeTestRule.scrollToOnShownIndex(index: Int = ON_SHOWN_INDEX) { + this.onNodeWithTag(ON_SHOWN_ROOT_TAG) + .performScrollToIndex(index) + } + + @Composable + private fun ModifierOnShownContent( + indexToValidate: Int = ON_SHOWN_INDEX, + settleTime: Int = ON_SHOWN_SETTLE_TIME_MS, + onVisible: () -> Unit, + ) { + LazyColumn( + modifier = Modifier.testTag(ON_SHOWN_ROOT_TAG), + ) { + items(ON_SHOWN_NODE_COUNT) { index -> + val modifier = if (index == indexToValidate) { + Modifier.onShown( + threshold = 1.0f, + settleTime = settleTime, + onVisible = onVisible, + ) + } else { + Modifier + } + + Text( + text = "Test item $index", + modifier = modifier + .fillMaxWidth() + .height(50.dp), + ) + } + } + } +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt index 987cea5818..17fd124d62 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt @@ -14,9 +14,9 @@ import org.junit.Rule import org.junit.Test import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.TestAssetHelper -import org.mozilla.fenix.helpers.TestHelper.runWithSystemLocaleChanged import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.navigationToolbar import java.util.Locale diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/NoNetworkAccessStartupTests.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/NoNetworkAccessStartupTests.kt index a33c39338d..fd30ae0ad6 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/NoNetworkAccessStartupTests.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/NoNetworkAccessStartupTests.kt @@ -10,9 +10,9 @@ import org.junit.Rule import org.junit.Test import org.mozilla.fenix.R import org.mozilla.fenix.customannotations.SmokeTest +import org.mozilla.fenix.helpers.AppAndSystemHelper.setNetworkEnabled import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.TestHelper.packageName -import org.mozilla.fenix.helpers.TestHelper.setNetworkEnabled import org.mozilla.fenix.helpers.TestHelper.verifyUrl import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.homeScreen diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/OnboardingTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/OnboardingTest.kt new file mode 100644 index 0000000000..9913b7769a --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/OnboardingTest.kt @@ -0,0 +1,70 @@ +package org.mozilla.fenix.ui + +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import org.junit.Rule +import org.junit.Test +import org.mozilla.fenix.customannotations.SmokeTest +import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithLauncherIntent +import org.mozilla.fenix.helpers.HomeActivityIntentTestRule +import org.mozilla.fenix.ui.robots.homeScreen + +class OnboardingTest { + + @get:Rule + val activityTestRule = + AndroidComposeTestRule( + HomeActivityIntentTestRule.withDefaultSettingsOverrides(launchActivity = false), + ) { it.activity } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2122321 + @Test + fun verifyFirstOnboardingCardItemsTest() { + runWithLauncherIntent(activityTestRule) { + homeScreen { + verifyFirstOnboardingCard(activityTestRule) + } + } + } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2122334 + @SmokeTest + @Test + fun verifyFirstOnboardingCardItemsFunctionalityTest() { + runWithLauncherIntent(activityTestRule) { + homeScreen { + clickNotNowOnboardingButton(activityTestRule) + verifySecondOnboardingCard(activityTestRule) + swipeSecondOnboardingCardToRight() + }.clickSetAsDefaultBrowserOnboardingButton(activityTestRule) { + verifyAndroidDefaultAppsMenuAppears() + }.goBackToOnboardingScreen { + verifySecondOnboardingCard(activityTestRule) + } + } + } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2122343 + @Test + fun verifySecondOnboardingCardItemsTest() { + runWithLauncherIntent(activityTestRule) { + homeScreen { + clickNotNowOnboardingButton(activityTestRule) + verifySecondOnboardingCard(activityTestRule) + } + } + } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2122344 + @SmokeTest + @Test + fun verifySecondOnboardingCardSignInFunctionalityTest() { + runWithLauncherIntent(activityTestRule) { + homeScreen { + clickNotNowOnboardingButton(activityTestRule) + verifySecondOnboardingCard(activityTestRule) + }.clickSignInOnboardingButton(activityTestRule) { + verifyTurnOnSyncMenu() + } + } + } +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/PDFViewerTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/PDFViewerTest.kt index adce7046e2..7b07993a29 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/PDFViewerTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/PDFViewerTest.kt @@ -14,6 +14,8 @@ import org.junit.Rule import org.junit.Test import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.assertExternalAppOpens +import org.mozilla.fenix.helpers.AppAndSystemHelper.clearDownloadsFolder import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_DOCS import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.MatcherHelper @@ -21,8 +23,6 @@ import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText import org.mozilla.fenix.helpers.MatcherHelper.itemWithText import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset -import org.mozilla.fenix.helpers.TestHelper.assertExternalAppOpens -import org.mozilla.fenix.helpers.TestHelper.deleteDownloadedFileOnStorage import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.ui.robots.clickPageObject import org.mozilla.fenix.ui.robots.navigationToolbar @@ -50,6 +50,9 @@ class PDFViewerTest { @After fun tearDown() { mockWebServer.shutdown() + + // Check and clear the downloads folder + clearDownloadsFolder() } // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2048140 @@ -96,7 +99,6 @@ class PDFViewerTest { }.clickOpen("application/pdf") { assertExternalAppOpens(GOOGLE_DOCS) } - deleteDownloadedFileOnStorage(downloadFile) } // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2283305 diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt index 77feeae197..aa2215fddb 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt @@ -15,10 +15,10 @@ import org.junit.Test import org.mozilla.fenix.R import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.RetryTestRule import org.mozilla.fenix.helpers.TestAssetHelper -import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.navigationToolbar diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/RecentlyClosedTabsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/RecentlyClosedTabsTest.kt index 0ee7b14eab..ca4084655b 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/RecentlyClosedTabsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/RecentlyClosedTabsTest.kt @@ -15,12 +15,12 @@ import org.junit.Test import org.mozilla.fenix.R import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.RecyclerViewIdlingResource import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset import org.mozilla.fenix.helpers.TestHelper.longTapSelectItem import org.mozilla.fenix.helpers.TestHelper.mDevice -import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.navigationToolbar diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt index bae3aa84da..c2bc8cae49 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt @@ -21,6 +21,12 @@ import org.junit.Test import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.helpers.AppAndSystemHelper.assertNativeAppOpens +import org.mozilla.fenix.helpers.AppAndSystemHelper.denyPermission +import org.mozilla.fenix.helpers.AppAndSystemHelper.grantSystemPermission +import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithCondition +import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged +import org.mozilla.fenix.helpers.AppAndSystemHelper.verifyKeyboardVisibility import org.mozilla.fenix.helpers.Constants.PackageName.ANDROID_SETTINGS import org.mozilla.fenix.helpers.Constants.searchEngineCodes import org.mozilla.fenix.helpers.HomeActivityTestRule @@ -32,17 +38,11 @@ import org.mozilla.fenix.helpers.MockBrowserDataHelper.createTabItem import org.mozilla.fenix.helpers.MockBrowserDataHelper.setCustomSearchEngine import org.mozilla.fenix.helpers.SearchDispatcher import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset -import org.mozilla.fenix.helpers.TestHelper import org.mozilla.fenix.helpers.TestHelper.appContext -import org.mozilla.fenix.helpers.TestHelper.assertNativeAppOpens import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton -import org.mozilla.fenix.helpers.TestHelper.denyPermission import org.mozilla.fenix.helpers.TestHelper.exitMenu -import org.mozilla.fenix.helpers.TestHelper.grantSystemPermission import org.mozilla.fenix.helpers.TestHelper.longTapSelectItem import org.mozilla.fenix.helpers.TestHelper.mDevice -import org.mozilla.fenix.helpers.TestHelper.runWithCondition -import org.mozilla.fenix.helpers.TestHelper.verifyKeyboardVisibility import org.mozilla.fenix.ui.robots.clickContextMenuItem import org.mozilla.fenix.ui.robots.clickPageObject import org.mozilla.fenix.ui.robots.homeScreen @@ -77,7 +77,6 @@ class SearchTest { isRecentTabsFeatureEnabled = false, isTCPCFREnabled = false, isWallpaperOnboardingEnabled = false, - isCookieBannerReductionDialogEnabled = false, tabsTrayRewriteEnabled = false, ), ) { it.activity } @@ -792,7 +791,7 @@ class SearchTest { fun verifySearchEnginesFunctionalityUsingRTLLocaleTest() { val arabicLocale = Locale("ar", "AR") - TestHelper.runWithSystemLocaleChanged(arabicLocale, activityTestRule.activityRule) { + runWithSystemLocaleChanged(arabicLocale, activityTestRule.activityRule) { homeScreen { }.openSearch { verifyTranslatedFocusedNavigationToolbar("ابحث أو أدخِل عنوانا") diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt index a030cde3bb..9645b5db7b 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt @@ -13,16 +13,15 @@ import org.mozilla.fenix.R import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.ext.settings import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.RecyclerViewIdlingResource import org.mozilla.fenix.helpers.TestAssetHelper.getEnhancedTrackingProtectionAsset -import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset -import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources +import org.mozilla.fenix.helpers.TestHelper import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText import org.mozilla.fenix.helpers.TestHelper.waitUntilSnackbarGone import org.mozilla.fenix.ui.robots.addonsMenu import org.mozilla.fenix.ui.robots.homeScreen -import org.mozilla.fenix.ui.robots.navigationToolbar /** * Tests for verifying the functionality of installing or removing addons @@ -101,8 +100,7 @@ class SettingsAddonsTest { val addonName = "uBlock Origin" addonsMenu { - installAddon(addonName) - verifyAddonInstallCompleted(addonName, activityTestRule) + installAddon(addonName, activityTestRule) closeAddonInstallCompletePrompt() }.openDetailedMenuForAddon(addonName) { }.removeAddon(activityTestRule) { @@ -115,25 +113,34 @@ class SettingsAddonsTest { } } + // TODO: Harden to dynamically install addons from position + // in list of detected addons on screen instead of hard-coded values. // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/561600 - // Installs uBlock add-on and checks that the app doesn't crash while loading pages with trackers + // Installs 2 add-on and checks that the app doesn't crash while navigating the app @SmokeTest @Test fun noCrashWithAddonInstalledTest() { // setting ETP to Strict mode to test it works with add-ons activityTestRule.activity.settings().setStrictETP() - val addonName = "uBlock Origin" + val uBlockAddon = "uBlock Origin" + val darkReaderAddon = "Dark Reader" val trackingProtectionPage = getEnhancedTrackingProtectionAsset(mockWebServer) addonsMenu { - installAddon(addonName) - verifyAddonInstallCompleted(addonName, activityTestRule) + installAddon(uBlockAddon, activityTestRule) + closeAddonInstallCompletePrompt() + installAddon(darkReaderAddon, activityTestRule) closeAddonInstallCompletePrompt() }.goBack { }.openNavigationToolbar { }.enterURLAndEnterToBrowser(trackingProtectionPage.url) { verifyUrl(trackingProtectionPage.url.toString()) + }.goToHomescreen { + }.openTopSiteTabWithTitle("Top Articles") { + }.openThreeDotMenu { + }.openSettings { + verifySettingsView() } } @@ -141,22 +148,38 @@ class SettingsAddonsTest { @SmokeTest @Test fun verifyUBlockWorksInPrivateModeTest() { + TestHelper.appContext.settings().shouldShowCookieBannersCFR = false val addonName = "uBlock Origin" - val genericPage = getGenericAsset(mockWebServer, 1) addonsMenu { - installAddon(addonName) - verifyAddonInstallCompleted(addonName, activityTestRule) + installAddon(addonName, activityTestRule) selectAllowInPrivateBrowsing() closeAddonInstallCompletePrompt() }.goBack { - }.togglePrivateBrowsingMode() - navigationToolbar { - }.enterURLAndEnterToBrowser(genericPage.url) { - verifyPageContent(genericPage.content) + }.openContextMenuOnSponsoredShortcut("Top Articles") { + }.openTopSiteInPrivateTab { + waitForPageToLoad() }.openThreeDotMenu { openAddonsSubList() verifyAddonAvailableInMainMenu(addonName) + verifyTrackersBlockedByUblock() + } + } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/875785 + @Test + fun verifyUBlockWorksInNormalModeTest() { + val addonName = "uBlock Origin" + + addonsMenu { + installAddon(addonName, activityTestRule) + closeAddonInstallCompletePrompt() + }.goBack { + }.openTopSiteTabWithTitle("Top Articles") { + waitForPageToLoad() + }.openThreeDotMenu { + openAddonsSubList() + verifyTrackersBlockedByUblock() } } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt index 7b8e71118a..7bcebac373 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt @@ -13,12 +13,14 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.mozilla.fenix.customannotations.SmokeTest +import org.mozilla.fenix.ext.settings import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.assertYoutubeAppOpens import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText import org.mozilla.fenix.helpers.TestAssetHelper -import org.mozilla.fenix.helpers.TestHelper.assertYoutubeAppOpens +import org.mozilla.fenix.helpers.TestHelper import org.mozilla.fenix.helpers.TestHelper.exitMenu import org.mozilla.fenix.ui.robots.clickPageObject import org.mozilla.fenix.ui.robots.homeScreen @@ -201,6 +203,7 @@ class SettingsAdvancedTest { // Assumes Youtube is installed and enabled @Test fun privateBrowsingAskBeforeOpeningLinkInAppCancelTest() { + TestHelper.appContext.settings().shouldShowCookieBannersCFR = false val externalLinksPage = TestAssetHelper.getExternalLinksAsset(mockWebServer) homeScreen { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataOnQuitTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataOnQuitTest.kt index d161098ddc..40a26ac531 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataOnQuitTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataOnQuitTest.kt @@ -16,17 +16,18 @@ import org.junit.Test import org.mozilla.fenix.R import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper +import org.mozilla.fenix.helpers.AppAndSystemHelper.setNetworkEnabled +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.MatcherHelper import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper.getStorageTestAsset -import org.mozilla.fenix.helpers.TestHelper -import org.mozilla.fenix.helpers.TestHelper.deleteDownloadedFileOnStorage import org.mozilla.fenix.helpers.TestHelper.exitMenu import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.restartApp -import org.mozilla.fenix.helpers.TestHelper.setNetworkEnabled import org.mozilla.fenix.ui.robots.clickPageObject +import org.mozilla.fenix.ui.robots.downloadRobot import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.navigationToolbar @@ -58,6 +59,9 @@ class SettingsDeleteBrowsingDataOnQuitTest { @After fun tearDown() { mockWebServer.shutdown() + + // Check and clear the downloads folder + AppAndSystemHelper.clearDownloadsFolder() } // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416048 @@ -191,13 +195,10 @@ class SettingsDeleteBrowsingDataOnQuitTest { clickDeleteBrowsingOnQuitButtonSwitch() exitMenu() } - navigationToolbar { - }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { - }.clickDownloadLink("smallZip.zip") { - verifyDownloadPrompt("smallZip.zip") - }.clickDownload { + downloadRobot { + openPageAndDownloadFile(url = downloadTestPage.toUri(), downloadFile = "smallZip.zip") verifyDownloadCompleteNotificationPopup() - }.closeCompletedDownloadPrompt { + }.closeDownloadPrompt { }.goToHomescreen { }.openThreeDotMenu { clickQuit() @@ -209,7 +210,6 @@ class SettingsDeleteBrowsingDataOnQuitTest { }.openDownloadsManager { verifyEmptyDownloadsList() } - deleteDownloadedFileOnStorage("smallZip.zip") } // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416053 @@ -251,7 +251,7 @@ class SettingsDeleteBrowsingDataOnQuitTest { // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/416052 @Test fun deleteCachedFilesOnQuitTest() { - val pocketTopArticles = TestHelper.getStringResource(R.string.pocket_pinned_top_articles) + val pocketTopArticles = getStringResource(R.string.pocket_pinned_top_articles) homeScreen { }.openThreeDotMenu { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataTest.kt index 0b89b8461b..8df4f3813b 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataTest.kt @@ -13,15 +13,15 @@ import org.junit.Test import org.mozilla.fenix.R import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.setNetworkEnabled +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper.getStorageTestAsset import org.mozilla.fenix.helpers.TestHelper.exitMenu -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.restartApp -import org.mozilla.fenix.helpers.TestHelper.setNetworkEnabled import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.clickPageObject import org.mozilla.fenix.ui.robots.homeScreen diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeveloperToolsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeveloperToolsTest.kt deleted file mode 100644 index e62ec58325..0000000000 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeveloperToolsTest.kt +++ /dev/null @@ -1,100 +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.Ignore -import org.junit.Rule -import org.junit.Test -import org.mozilla.fenix.helpers.AndroidAssetDispatcher -import org.mozilla.fenix.helpers.HomeActivityTestRule -import org.mozilla.fenix.ui.robots.homeScreen - -/** - * Tests for verifying the main three dot menu options - * - */ - -class SettingsDeveloperToolsTest { - private lateinit var mDevice: UiDevice - private lateinit var mockWebServer: MockWebServer - - @get:Rule - val activityTestRule = HomeActivityTestRule() - - @Before - fun setUp() { - mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - mockWebServer = MockWebServer().apply { - dispatcher = AndroidAssetDispatcher() - start() - } - } - - @After - fun tearDown() { - mockWebServer.shutdown() - } - - // Walks through settings developer tools menu and sub-menus to ensure all items are present - @Test - fun settingsDeveloperToolsItemsTest() { - homeScreen { - }.openThreeDotMenu { - }.openSettings { - verifyRemoteDebuggingButton() - } - } - - // DEVELOPER TOOLS - @Ignore("This is a stub test, ignore for now") - @Test - fun turnOnRemoteDebuggingViaUsb() { - // Open terminal - // Verify USB debugging is off - // Open 3dot (main) menu - // Select settings - // Toggle Remote debugging via USB to 'on' - // Open terminal - // Verify USB debugging is on - } - - // ABOUT - @Ignore("This is a stub test, ignore for now") - @Test - fun verifyHelpRedirect() { - // Open 3dot (main) menu - // Select settings - // Click on "Help" - // Verify redirect to: https://support.mozilla.org/ - } - - @Ignore("This is a stub test, ignore for now") - @Test - fun verifyRateOnGooglePlayRedirect() { - // Open 3dot (main) menu - // Select settings - // Click on "Rate on Google Play" - // Verify Android "Open with Google Play Store" sub menu - } - - @Ignore("This is a stub test, ignore for now") - @Test - fun verifyAboutFirefoxPreview() { - // Open 3dot (main) menu - // Select settings - // Click on "Verify About Firefox Preview" - // Verify about page contains.... - // Build # - // Version # - // "Firefox Preview is produced by Mozilla" - // Day, Date, timestamp - // "Open source libraries we use" - } -} diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsGeneralTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsGeneralTest.kt index 8e4c165776..b4423a2729 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsGeneralTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsGeneralTest.kt @@ -14,14 +14,14 @@ import org.mozilla.fenix.FenixApplication import org.mozilla.fenix.R import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources +import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.RecyclerViewIdlingResource import org.mozilla.fenix.helpers.TestAssetHelper.getLoremIpsumAsset import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeLong -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice -import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources -import org.mozilla.fenix.helpers.TestHelper.runWithSystemLocaleChanged import org.mozilla.fenix.ui.robots.checkTextSizeOnWebsite import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.util.FRENCH_LANGUAGE_HEADER diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsHomepageTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsHomepageTest.kt index 769add7bf6..4c525f0965 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsHomepageTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsHomepageTest.kt @@ -12,11 +12,11 @@ import org.junit.Rule import org.junit.Test import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.openAppFromExternalLink import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.RetryTestRule import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset import org.mozilla.fenix.helpers.TestHelper.mDevice -import org.mozilla.fenix.helpers.TestHelper.openAppFromExternalLink import org.mozilla.fenix.helpers.TestHelper.restartApp import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.homeScreen @@ -103,12 +103,12 @@ class SettingsHomepageTest { navigationToolbar { }.enterURLAndEnterToBrowser(genericURL.url) { }.goToHomescreen { - verifyRecentlyVisitedSectionIsDisplayed() + verifyRecentlyVisitedSectionIsDisplayed(true) }.openThreeDotMenu { }.openCustomizeHome { clickRecentlyVisited() }.goBackToHomeScreen { - verifyRecentlyVisitedSectionIsNotDisplayed() + verifyRecentlyVisitedSectionIsDisplayed(false) } } @@ -141,12 +141,12 @@ class SettingsHomepageTest { }.openThreeDotMenu { }.bookmarkPage { }.goToHomescreen { - verifyRecentBookmarksSectionIsDisplayed() + verifyRecentBookmarksSectionIsDisplayed(exists = true) }.openThreeDotMenu { }.openCustomizeHome { clickRecentBookmarksButton() }.goBackToHomeScreen { - verifyRecentBookmarksSectionIsNotDisplayed() + verifyRecentBookmarksSectionIsDisplayed(exists = false) } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivacyTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivacyTest.kt index c70c75df62..bbfe7cb919 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivacyTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivacyTest.kt @@ -55,8 +55,7 @@ class SettingsPrivacyTest { verifyPrivateBrowsingButton() verifyHTTPSOnlyModeButton() verifySettingsOptionSummary("HTTPS-Only Mode", "Off") - verifyCookieBannerReductionButton() - verifySettingsOptionSummary("Cookie banner reduction", "Off") + verifySettingsOptionSummary("Cookie Banner Blocker in private browsing", "") verifyEnhancedTrackingProtectionButton() verifySettingsOptionSummary("Enhanced Tracking Protection", "Standard") verifySitePermissionsButton() diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivateBrowsingTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivateBrowsingTest.kt index 0add122889..bf52c2748e 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivateBrowsingTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivateBrowsingTest.kt @@ -10,9 +10,10 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.openAppFromExternalLink +import org.mozilla.fenix.helpers.DataGenerationHelper.generateRandomString import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.TestAssetHelper -import org.mozilla.fenix.helpers.TestHelper import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.restartApp import org.mozilla.fenix.ui.robots.addToHomeScreen @@ -22,7 +23,7 @@ import org.mozilla.fenix.ui.robots.navigationToolbar class SettingsPrivateBrowsingTest { private lateinit var mockWebServer: MockWebServer - private val pageShortcutName = TestHelper.generateRandomString(5) + private val pageShortcutName = generateRandomString(5) @get:Rule val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true) @@ -63,7 +64,7 @@ class SettingsPrivateBrowsingTest { setOpenLinksInPrivateOn() - TestHelper.openAppFromExternalLink(firstWebPage.url.toString()) + openAppFromExternalLink(firstWebPage.url.toString()) browserScreen { verifyUrl(firstWebPage.url.toString()) @@ -75,7 +76,7 @@ class SettingsPrivateBrowsingTest { setOpenLinksInPrivateOff() // We need to open a different link, otherwise it will open the same session - TestHelper.openAppFromExternalLink(secondWebPage.url.toString()) + openAppFromExternalLink(secondWebPage.url.toString()) browserScreen { verifyUrl(secondWebPage.url.toString()) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSearchTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSearchTest.kt index e94ecf8439..4c5a1a1973 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSearchTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSearchTest.kt @@ -14,6 +14,9 @@ import org.junit.Rule import org.junit.Test import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged +import org.mozilla.fenix.helpers.AppAndSystemHelper.setSystemLocale +import org.mozilla.fenix.helpers.DataGenerationHelper.setTextToClipBoard import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.MockBrowserDataHelper.addCustomSearchEngine import org.mozilla.fenix.helpers.MockBrowserDataHelper.createBookmarkItem @@ -23,9 +26,6 @@ import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset import org.mozilla.fenix.helpers.TestHelper.appContext import org.mozilla.fenix.helpers.TestHelper.exitMenu import org.mozilla.fenix.helpers.TestHelper.restartApp -import org.mozilla.fenix.helpers.TestHelper.runWithSystemLocaleChanged -import org.mozilla.fenix.helpers.TestHelper.setSystemLocale -import org.mozilla.fenix.helpers.TestHelper.setTextToClipBoard import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText import org.mozilla.fenix.ui.robots.EngineShortcut import org.mozilla.fenix.ui.robots.homeScreen diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSitePermissionsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSitePermissionsTest.kt index 42dd55e859..8dcfdc573c 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSitePermissionsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSitePermissionsTest.kt @@ -18,13 +18,13 @@ import org.junit.Test import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.ext.components import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.grantSystemPermission import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.MatcherHelper.itemWithText import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset import org.mozilla.fenix.helpers.TestAssetHelper.getMutedVideoPageAsset import org.mozilla.fenix.helpers.TestAssetHelper.getVideoPageAsset import org.mozilla.fenix.helpers.TestHelper.exitMenu -import org.mozilla.fenix.helpers.TestHelper.grantSystemPermission import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.clickPageObject import org.mozilla.fenix.ui.robots.homeScreen diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSyncTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSyncTest.kt deleted file mode 100644 index 86777960c3..0000000000 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSyncTest.kt +++ /dev/null @@ -1,68 +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.Ignore -import org.junit.Rule -import org.junit.Test -import org.mozilla.fenix.helpers.AndroidAssetDispatcher -import org.mozilla.fenix.helpers.HomeActivityTestRule - -/** - * Tests for verifying the main three dot menu options - * - */ - -class SettingsSyncTest { - private lateinit var mDevice: UiDevice - private lateinit var mockWebServer: MockWebServer - - @get:Rule - val activityTestRule = HomeActivityTestRule() - - @Before - fun setUp() { - mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - mockWebServer = MockWebServer().apply { - dispatcher = AndroidAssetDispatcher() - start() - } - } - - @After - fun tearDown() { - mockWebServer.shutdown() - } - - // Walks through settings sync menu and sub-menus to ensure all items are present - @Ignore("This is a stub test, ignore for now") - @Test - fun settingsSyncItemsTest() { - // SYNC - - // Open 3dot (main) menu - // Select settings - // Verify header: "Turn on Sync" - // Verify description: "Sync bookmarks, history, and more with your Firefox Account" - } - - // SYNC - @Ignore("This is a stub test, ignore for now") - @Test - fun turnOnSync() { - // Note this requires a test Firefox Account and a desktop - // Open 3dot (main) menu - // Select settings - // Click on "Turn on Sync" - // Open Firefox on laptop and go to https://firefox.com/pair - // Pair with QR code and/or alternate method - // Verify pairing - } -} diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SitePermissionsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SitePermissionsTest.kt index 4c28040fa7..d03c36f251 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SitePermissionsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SitePermissionsTest.kt @@ -20,14 +20,14 @@ import org.junit.Rule import org.junit.Test import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.assertExternalAppOpens +import org.mozilla.fenix.helpers.AppAndSystemHelper.grantSystemPermission import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.MockLocationUpdatesRule import org.mozilla.fenix.helpers.RetryTestRule import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestHelper.appContext -import org.mozilla.fenix.helpers.TestHelper.assertExternalAppOpens -import org.mozilla.fenix.helpers.TestHelper.grantSystemPermission import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.clickPageObject import org.mozilla.fenix.ui.robots.navigationToolbar diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SponsoredShortcutsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SponsoredShortcutsTest.kt index 28a747d868..ee483f98e6 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SponsoredShortcutsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SponsoredShortcutsTest.kt @@ -15,9 +15,9 @@ import org.junit.Test import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.Constants.defaultTopSitesList +import org.mozilla.fenix.helpers.DataGenerationHelper.getSponsoredShortcutTitle import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.TestAssetHelper -import org.mozilla.fenix.helpers.TestHelper.getSponsoredShortcutTitle import org.mozilla.fenix.ui.robots.homeScreen /** diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt index ea196f189c..d09ac4e0c4 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt @@ -23,7 +23,6 @@ import org.mozilla.fenix.helpers.RetryTestRule import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestHelper.closeApp import org.mozilla.fenix.helpers.TestHelper.restartApp -import org.mozilla.fenix.helpers.TestHelper.verifyKeyboardVisibility import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.clickPageObject import org.mozilla.fenix.ui.robots.homeScreen @@ -77,7 +76,7 @@ class TabbedBrowsingTest { mockWebServer.shutdown() } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903599 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903599 @Test fun closeAllTabsTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -110,9 +109,9 @@ class TabbedBrowsingTest { } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903604 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2349580 @Test - fun closingTabsMethodsTest() { + fun closingTabsTest() { val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1) navigationToolbar { @@ -125,13 +124,17 @@ class TabbedBrowsingTest { } browserScreen { verifyTabCounter("1") - }.openTabDrawer { - closeTab() } - homeScreen { - verifyTabCounter("0") - }.openNavigationToolbar { + } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903604 + @Test + fun swipeToCloseTabsTest() { + val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1) + + navigationToolbar { }.enterURLAndEnterToBrowser(genericURL.url) { + waitForPageToLoad() }.openTabDrawer { verifyExistingOpenTabs("Test_Page_1") swipeTabRight("Test_Page_1") @@ -141,6 +144,7 @@ class TabbedBrowsingTest { verifyTabCounter("0") }.openNavigationToolbar { }.enterURLAndEnterToBrowser(genericURL.url) { + waitForPageToLoad() }.openTabDrawer { verifyExistingOpenTabs("Test_Page_1") swipeTabLeft("Test_Page_1") @@ -151,12 +155,12 @@ class TabbedBrowsingTest { } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903591 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903591 @Test - fun closingPrivateTabsMethodsTest() { + fun closingPrivateTabsTest() { val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1) - homeScreen { }.togglePrivateBrowsingMode() + homeScreen { }.togglePrivateBrowsingMode(switchPBModeOn = true) navigationToolbar { }.enterURLAndEnterToBrowser(genericURL.url) { }.openTabDrawer { @@ -168,33 +172,10 @@ class TabbedBrowsingTest { } browserScreen { verifyTabCounter("1") - }.openTabDrawer { - closeTab() - } - homeScreen { - verifyTabCounter("0") - }.openNavigationToolbar { - }.enterURLAndEnterToBrowser(genericURL.url) { - }.openTabDrawer { - verifyExistingOpenTabs("Test_Page_1") - swipeTabRight("Test_Page_1") - verifySnackBarText("Private tab closed") - } - homeScreen { - verifyTabCounter("0") - }.openNavigationToolbar { - }.enterURLAndEnterToBrowser(genericURL.url) { - }.openTabDrawer { - verifyExistingOpenTabs("Test_Page_1") - swipeTabLeft("Test_Page_1") - verifySnackBarText("Private tab closed") - } - homeScreen { - verifyTabCounter("0") } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903606 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903606 @SmokeTest @Test fun tabMediaControlButtonTest() { @@ -235,7 +216,7 @@ class TabbedBrowsingTest { } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903598 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903598 @SmokeTest @Test fun shareTabsFromTabsTrayTest() { @@ -269,6 +250,7 @@ class TabbedBrowsingTest { } } + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903602 @Test fun verifyTabTrayNotShowingStateHalfExpanded() { navigationToolbar { @@ -292,7 +274,7 @@ class TabbedBrowsingTest { } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903600 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903600 @Test fun verifyEmptyTabTray() { navigationToolbar { @@ -307,7 +289,7 @@ class TabbedBrowsingTest { } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903585 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903585 @Test fun verifyEmptyPrivateTabsTrayTest() { navigationToolbar { @@ -323,7 +305,7 @@ class TabbedBrowsingTest { } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903601 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903601 @Test fun verifyTabsTrayWithOpenTabTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -349,7 +331,7 @@ class TabbedBrowsingTest { } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/903587 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/903587 @SmokeTest @Test fun verifyPrivateTabsTrayWithOpenTabTest() { @@ -374,80 +356,133 @@ class TabbedBrowsingTest { } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/927315 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/927314 @Test - fun tabsCounterShortcutMenuTest() { - val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + fun tabsCounterShortcutMenuCloseTabTest() { + val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2) navigationToolbar { - }.enterURLAndEnterToBrowser(defaultWebPage.url) {} + }.enterURLAndEnterToBrowser(firstWebPage.url) { + waitForPageToLoad() + }.goToHomescreen { + }.openNavigationToolbar { + }.enterURLAndEnterToBrowser(secondWebPage.url) { + waitForPageToLoad() + } navigationToolbar { }.openTabButtonShortcutsMenu { verifyTabButtonShortcutMenuItems() }.closeTabFromShortcutsMenu { + browserScreen { + verifyTabCounter("1") + verifyPageContent(firstWebPage.content) + } + } + } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2343663 + @Test + fun tabsCounterShortcutMenuNewPrivateTabTest() { + val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + + navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) {} navigationToolbar { }.openTabButtonShortcutsMenu { }.openNewPrivateTabFromShortcutsMenu { - verifyKeyboardVisibility() verifySearchBarPlaceholder("Search or enter address") - // dismiss search dialog }.dismissSearchBar { - verifyPrivateBrowsingHomeScreenItems() + verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = true) } + } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2343662 + @Test + fun tabsCounterShortcutMenuNewTabTest() { + val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) {} navigationToolbar { }.openTabButtonShortcutsMenu { }.openNewTabFromShortcutsMenu { - verifyKeyboardVisibility() verifySearchBarPlaceholder("Search or enter address") - // dismiss search dialog }.dismissSearchBar { - verifyHomeWordmark() - verifyNavigationToolbar() + verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = false) } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/927314 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/927315 @Test - fun privateTabsCounterShortcutMenuTest() { - val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + fun privateTabsCounterShortcutMenuCloseTabTest() { + val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2) - homeScreen {}.togglePrivateBrowsingMode() + homeScreen {}.togglePrivateBrowsingMode(switchPBModeOn = true) navigationToolbar { - }.enterURLAndEnterToBrowser(defaultWebPage.url) { + }.enterURLAndEnterToBrowser(firstWebPage.url) { + waitForPageToLoad() + }.goToHomescreen { + }.openNavigationToolbar { + }.enterURLAndEnterToBrowser(secondWebPage.url) { waitForPageToLoad() } navigationToolbar { }.openTabButtonShortcutsMenu { verifyTabButtonShortcutMenuItems() }.closeTabFromShortcutsMenu { - }.enterURLAndEnterToBrowser(defaultWebPage.url) {} + browserScreen { + verifyTabCounter("1") + verifyPageContent(firstWebPage.content) + } + }.openTabButtonShortcutsMenu { + }.closeTabFromShortcutsMenu { + homeScreen { + verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = true) + } + } + } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2344199 + @Test + fun privateTabsCounterShortcutMenuNewPrivateTabTest() { + val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + + homeScreen {}.togglePrivateBrowsingMode(switchPBModeOn = true) + navigationToolbar { + }.enterURLAndEnterToBrowser(defaultWebPage.url) { + waitForPageToLoad() + } navigationToolbar { }.openTabButtonShortcutsMenu { }.openNewPrivateTabFromShortcutsMenu { - verifyKeyboardVisibility() verifySearchBarPlaceholder("Search or enter address") - // dismiss search dialog }.dismissSearchBar { - verifyCommonMythsLink() + verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = true) } + } + + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2344198 + @Test + fun privateTabsCounterShortcutMenuNewTabTest() { + val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + + homeScreen {}.togglePrivateBrowsingMode(switchPBModeOn = true) navigationToolbar { - }.enterURLAndEnterToBrowser(defaultWebPage.url) {} + }.enterURLAndEnterToBrowser(defaultWebPage.url) { + verifyPageContent(defaultWebPage.content) + } navigationToolbar { }.openTabButtonShortcutsMenu { }.openNewTabFromShortcutsMenu { - verifyKeyboardVisibility() - verifySearchBarPlaceholder("Search or enter address") - // dismiss search dialog + verifySearchToolbar(isDisplayed = true) }.dismissSearchBar { - // Verify normal browsing homescreen - verifyExistingTopSitesList() + verifyIfInPrivateOrNormalMode(privateBrowsingEnabled = false) } } - // TestRail: https://testrail.stage.mozaws.net/index.php?/cases/view/1046683 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1046683 @Test fun verifySyncedTabsWhenUserIsNotSignedInTest() { navigationToolbar { @@ -495,12 +530,9 @@ class TabbedBrowsingTest { }.enterURLAndEnterToBrowser(firstWebPage.url) { }.openTabDrawer { }.openNewTab { - }.submitQuery(secondWebPage.url.toString()) { - } - + }.submitQuery(secondWebPage.url.toString()) {} closeApp(activityTestRule) restartApp(activityTestRule) - homeScreen { verifyPrivateBrowsingHomeScreenItems() }.openTabDrawer { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/TopSitesTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/TopSitesTest.kt index 4a273bfe4e..7858a52243 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/TopSitesTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/TopSitesTest.kt @@ -15,11 +15,11 @@ import org.mozilla.fenix.R import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.Constants.defaultTopSitesList +import org.mozilla.fenix.helpers.DataGenerationHelper.generateRandomString +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton -import org.mozilla.fenix.helpers.TestHelper.generateRandomString -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.waitUntilSnackbarGone import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.homeScreen diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/TotalCookieProtectionTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/TotalCookieProtectionTest.kt index c2bd3acf27..3eb7068c37 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/TotalCookieProtectionTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/TotalCookieProtectionTest.kt @@ -10,6 +10,7 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test +import org.mozilla.fenix.components.toolbar.CFR_MINIMUM_NUMBER_OPENED_TABS import org.mozilla.fenix.ext.settings import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.HomeActivityTestRule @@ -28,12 +29,12 @@ class TotalCookieProtectionTest { val composeTestRule = AndroidComposeTestRule( HomeActivityTestRule( isTCPCFREnabled = true, - isCookieBannerReductionDialogEnabled = false, ), ) { it.activity } @Before fun setUp() { + CFR_MINIMUM_NUMBER_OPENED_TABS = 0 mockWebServer = MockWebServer().apply { dispatcher = AndroidAssetDispatcher() start() @@ -43,6 +44,7 @@ class TotalCookieProtectionTest { @After fun tearDown() { mockWebServer.shutdown() + CFR_MINIMUM_NUMBER_OPENED_TABS = 5 } // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2260552 diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/UpgradingUsersOnboardingTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/UpgradingUsersOnboardingTest.kt index fa822cd33d..98f912bc79 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/UpgradingUsersOnboardingTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/UpgradingUsersOnboardingTest.kt @@ -24,7 +24,7 @@ class UpgradingUsersOnboardingTest { HomeActivityIntentTestRule(isHomeOnboardingDialogEnabled = true), ) { it.activity } - // https://testrail.stage.mozaws.net/index.php?/cases/view/1913592 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1913592 @Test fun upgradingUsersOnboardingScreensTest() { homeScreen { @@ -34,7 +34,7 @@ class UpgradingUsersOnboardingTest { } } - // https://testrail.stage.mozaws.net/index.php?/cases/view/1913591 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1913591 @Test fun upgradingUsersOnboardingCanBeSkippedTest() { homeScreen { @@ -50,7 +50,7 @@ class UpgradingUsersOnboardingTest { } } - // https://testrail.stage.mozaws.net/index.php?/cases/view/1932156 + // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1932156 @Test fun upgradingUsersOnboardingSignInButtonTest() { homeScreen { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/WebControlsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/WebControlsTest.kt index 2ce27be280..8375b88621 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/WebControlsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/WebControlsTest.kt @@ -10,6 +10,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.AppAndSystemHelper.assertNativeAppOpens import org.mozilla.fenix.helpers.Constants import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText @@ -18,7 +19,6 @@ import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper.getHTMLControlsFormAsset -import org.mozilla.fenix.helpers.TestHelper.assertNativeAppOpens import org.mozilla.fenix.ui.robots.clickPageObject import org.mozilla.fenix.ui.robots.navigationToolbar import java.time.LocalDate diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/AddToHomeScreenRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/AddToHomeScreenRobot.kt index 13bd3f9af6..5d0c83e9af 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/AddToHomeScreenRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/AddToHomeScreenRobot.kt @@ -13,8 +13,8 @@ import androidx.test.uiautomator.By import androidx.test.uiautomator.UiScrollable import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.Until -import org.junit.Assert.assertTrue -import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime @@ -38,7 +38,7 @@ class AddToHomeScreenRobot { fun addShortcutName(title: String) = shortcutTextField.setText(title) - fun verifyShortcutTextFieldTitle(title: String) = assertItemContainingTextExists(shortcutTitle(title)) + fun verifyShortcutTextFieldTitle(title: String) = assertUIObjectExists(shortcutTitle(title)) fun clickAddShortcutButton() = confirmAddToHomeScreenButton.clickAndWaitForNewWindow(waitingTime) @@ -60,12 +60,8 @@ class AddToHomeScreenRobot { } } - fun verifyShortcutAdded(shortcutTitle: String) { - assertTrue( - mDevice.findObject(UiSelector().text(shortcutTitle)) - .waitForExists(waitingTime), - ) - } + fun verifyShortcutAdded(shortcutTitle: String) = + assertUIObjectExists(itemContainingText(shortcutTitle)) class Transition { fun openHomeScreenShortcut(title: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt index f82de65c0f..7247f0075f 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt @@ -31,20 +31,18 @@ import androidx.test.uiautomator.Until import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.containsString import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse import org.mozilla.fenix.R -import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithDescriptionExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText +import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithText import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.click @@ -101,17 +99,16 @@ class BookmarksRobot { fun verifyCopySnackBarText() = assertSnackBarText("URL copied") - fun verifyEditBookmarksView() { - assertItemWithDescriptionExists(itemWithDescription("Navigate up")) - assertItemContainingTextExists(itemWithText(getStringResource(R.string.edit_bookmark_fragment_title))) - assertItemWithResIdExists( + fun verifyEditBookmarksView() = + assertUIObjectExists( + itemWithDescription("Navigate up"), + itemWithText(getStringResource(R.string.edit_bookmark_fragment_title)), itemWithResId("$packageName:id/delete_bookmark_button"), itemWithResId("$packageName:id/save_bookmark_button"), itemWithResId("$packageName:id/bookmarkNameEdit"), itemWithResId("$packageName:id/bookmarkUrlEdit"), itemWithResId("$packageName:id/bookmarkParentFolderSelector"), ) - } fun verifyKeyboardHidden() = assertKeyboardVisibility(isExpectedToBeVisible = false) @@ -260,7 +257,7 @@ class BookmarksRobot { } fun verifySearchedBookmarkExists(bookmarkUrl: String, exists: Boolean = true) = - assertItemContainingTextExists(itemContainingText(bookmarkUrl), exists = exists) + assertUIObjectExists(itemContainingText(bookmarkUrl), exists = exists) fun dismissBookmarksSearchBar() = mDevice.pressBack() @@ -403,12 +400,7 @@ private fun assertBookmarkFolderIsNotCreated(title: String) { .resourceId("$packageName:id/bookmarks_wrapper"), ).waitForExists(waitingTime) - assertFalse( - mDevice.findObject( - UiSelector() - .textContains(title), - ).waitForExists(waitingTimeShort), - ) + assertUIObjectExists(itemContainingText(title), exists = false) } private fun assertBookmarkFavicon(forUrl: Uri) = bookmarkFavicon(forUrl.toString()).check( @@ -434,12 +426,12 @@ private fun assertBookmarkIsDeleted(expectedTitle: String) { .resourceId("$packageName:id/bookmarks_wrapper"), ).waitForExists(waitingTime) - assertFalse( - mDevice.findObject( - UiSelector() - .resourceId("$packageName:id/title") - .textContains(expectedTitle), - ).waitForExists(waitingTimeShort), + assertUIObjectExists( + itemWithResIdContainingText( + "$packageName:id/title", + expectedTitle, + ), + exists = false, ) } private fun assertUndoDeleteSnackBarButton() = diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt index cadb54e26e..338876d457 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewInteraction import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.PickerActions @@ -41,10 +42,12 @@ import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION import org.mozilla.fenix.helpers.Constants.RETRY_COUNT +import org.mozilla.fenix.helpers.Constants.TAG +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.HomeActivityComposeTestRule -import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndTextExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists +import org.mozilla.fenix.helpers.MatcherHelper.assertItemTextEquals +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectIsGone import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId @@ -56,7 +59,6 @@ import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeLong import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort import org.mozilla.fenix.helpers.TestHelper.appName -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.TestHelper.waitForObjects @@ -68,7 +70,7 @@ import java.time.LocalDate class BrowserRobot { private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource - fun waitForPageToLoad() = progressBar.waitUntilGone(waitingTime) + fun waitForPageToLoad() = assertUIObjectIsGone(progressBar()) fun verifyCurrentPrivateSession(context: Context) { val selectedTab = context.components.core.store.state.selectedTab @@ -79,12 +81,11 @@ class BrowserRobot { sessionLoadedIdlingResource = SessionLoadedIdlingResource() runWithIdleRes(sessionLoadedIdlingResource) { - assertTrue( - mDevice.findObject( - UiSelector() - .resourceId("$packageName:id/mozac_browser_toolbar_url_view") - .textContains(url.replace("http://", "")), - ).waitForExists(waitingTime), + assertUIObjectExists( + itemWithResIdContainingText( + "$packageName:id/mozac_browser_toolbar_url_view", + url.replace("http://", ""), + ), ) } } @@ -105,7 +106,6 @@ class BrowserRobot { * document.querySelector('#testContent').innerText == expectedText * */ - fun verifyPageContent(expectedText: String) { sessionLoadedIdlingResource = SessionLoadedIdlingResource() @@ -115,10 +115,7 @@ class BrowserRobot { ) runWithIdleRes(sessionLoadedIdlingResource) { - assertTrue( - "Page didn't load or doesn't contain the expected text", - mDevice.findObject(UiSelector().textContains(expectedText)).waitForExists(waitingTime), - ) + assertUIObjectExists(itemContainingText(expectedText)) } } @@ -150,7 +147,7 @@ class BrowserRobot { for (i in 1..RETRY_COUNT) { try { - assertTrue(cacheSizeInfo.waitForExists(waitingTime)) + assertUIObjectExists(cacheSizeInfo) break } catch (e: AssertionError) { browserScreen { @@ -160,63 +157,55 @@ class BrowserRobot { } } - fun verifyTabCounter(expectedText: String) { - val counter = - mDevice.findObject( - UiSelector() - .resourceId("$packageName:id/counter_text") - .text(expectedText), - ) - assertTrue(counter.waitForExists(waitingTime)) - } + fun verifyTabCounter(expectedText: String) = + assertUIObjectExists( + itemWithResIdContainingText( + "$packageName:id/counter_text", + expectedText, + ), + ) fun verifySnackBarText(expectedText: String) { mDevice.waitForObjects(mDevice.findObject(UiSelector().textContains(expectedText))) - - assertTrue( - mDevice.findObject( - UiSelector() - .textContains(expectedText), - ).waitForExists(waitingTime), - ) + assertUIObjectExists(itemContainingText(expectedText)) } fun verifyContextMenuForLocalHostLinks(containsURL: Uri) { // If the link is directing to another local asset the "Download link" option is not available // If the link is not re-directing to an external app the "Open link in external app" option is not available - assertItemContainingTextExists( + assertUIObjectExists( contextMenuLinkUrl(containsURL.toString()), - contextMenuOpenLinkInNewTab, - contextMenuOpenLinkInPrivateTab, - contextMenuCopyLink, - contextMenuShareLink, + contextMenuOpenLinkInNewTab(), + contextMenuOpenLinkInPrivateTab(), + contextMenuCopyLink(), + contextMenuShareLink(), ) } fun verifyContextMenuForLinksToOtherApps(containsURL: String) { // If the link is re-directing to an external app the "Open link in external app" option is available // If the link is not directing to another local asset the "Download link" option is not available - assertItemContainingTextExists( + assertUIObjectExists( contextMenuLinkUrl(containsURL), - contextMenuOpenLinkInNewTab, - contextMenuOpenLinkInPrivateTab, - contextMenuCopyLink, - contextMenuDownloadLink, - contextMenuShareLink, - contextMenuOpenInExternalApp, + contextMenuOpenLinkInNewTab(), + contextMenuOpenLinkInPrivateTab(), + contextMenuCopyLink(), + contextMenuDownloadLink(), + contextMenuShareLink(), + contextMenuOpenInExternalApp(), ) } fun verifyContextMenuForLinksToOtherHosts(containsURL: Uri) { // If the link is re-directing to another host the "Download link" option is available // If the link is not re-directing to an external app the "Open link in external app" option is not available - assertItemContainingTextExists( + assertUIObjectExists( contextMenuLinkUrl(containsURL.toString()), - contextMenuOpenLinkInNewTab, - contextMenuOpenLinkInPrivateTab, - contextMenuCopyLink, - contextMenuDownloadLink, - contextMenuShareLink, + contextMenuOpenLinkInNewTab(), + contextMenuOpenLinkInPrivateTab(), + contextMenuCopyLink(), + contextMenuDownloadLink(), + contextMenuShareLink(), ) } @@ -243,8 +232,7 @@ class BrowserRobot { ) } - fun verifyNavURLBarHidden() = - assertTrue(navURLBar().waitUntilGone(waitingTime)) + fun verifyNavURLBarHidden() = assertUIObjectIsGone(navURLBar()) fun verifySecureConnectionLockIcon() = onView(withId(R.id.mozac_browser_toolbar_security_indicator)) @@ -265,21 +253,17 @@ class BrowserRobot { ) } - fun verifyNotificationDotOnMainMenu() { - assertTrue( - mDevice.findObject(UiSelector().resourceId("$packageName:id/notification_dot")) - .waitForExists(waitingTime), - ) - } + fun verifyNotificationDotOnMainMenu() = + assertUIObjectExists(itemWithResId("$packageName:id/notification_dot")) fun verifyHomeScreenButton() = homeScreenButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) - fun verifySearchBar() = assertTrue(searchBar().waitForExists(waitingTime)) + fun verifySearchBar() = assertUIObjectExists(searchBar()) fun dismissContentContextMenu() { mDevice.pressBack() - assertItemWithResIdExists(itemWithResId("$packageName:id/engineView")) + assertUIObjectExists(itemWithResId("$packageName:id/engineView")) } fun createBookmark(url: Uri, folder: String? = null) { @@ -300,20 +284,14 @@ class BrowserRobot { fun longClickPDFImage() = longClickPageObject(itemWithResId("pdfjs_internal_id_13R")) - fun verifyPDFReaderToolbarItems() { - assertTrue( - itemWithResIdAndText("download", "Download") - .waitForExists(waitingTime), + fun verifyPDFReaderToolbarItems() = + assertUIObjectExists( + itemWithResIdContainingText("download", "Download"), + itemWithResIdContainingText("openInApp", "Open in app"), ) - assertTrue( - itemWithResIdAndText("openInApp", "Open in app") - .waitForExists(waitingTime), - ) - } - fun clickSubmitLoginButton() { clickPageObject(itemWithResId("submit")) - itemWithResId("submit").waitUntilGone(waitingTime) + assertUIObjectIsGone(itemWithResId("submit")) mDevice.waitForIdle(waitingTimeLong) } @@ -321,7 +299,7 @@ class BrowserRobot { clickPageObject(itemWithResId("password")) setPageObjectText(itemWithResId("password"), password) - assertTrue(mDevice.findObject(UiSelector().text(password)).waitUntilGone(waitingTime)) + assertUIObjectIsGone(itemWithText(password)) } /** @@ -357,10 +335,10 @@ class BrowserRobot { // failing to swipe on Firebase sometimes, so it tries again try { navURLBar().swipeRight(2) - assertTrue(mDevice.findObject(UiSelector().text(tabUrl)).waitUntilGone(waitingTime)) + assertUIObjectIsGone(itemWithText(tabUrl)) } catch (e: AssertionError) { navURLBar().swipeRight(2) - assertTrue(mDevice.findObject(UiSelector().text(tabUrl)).waitUntilGone(waitingTime)) + assertUIObjectIsGone(itemWithText(tabUrl)) } } @@ -368,19 +346,19 @@ class BrowserRobot { // failing to swipe on Firebase sometimes, so it tries again try { navURLBar().swipeLeft(2) - assertTrue(mDevice.findObject(UiSelector().text(tabUrl)).waitUntilGone(waitingTime)) + assertUIObjectIsGone(itemWithText(tabUrl)) } catch (e: AssertionError) { navURLBar().swipeLeft(2) - assertTrue(mDevice.findObject(UiSelector().text(tabUrl)).waitUntilGone(waitingTime)) + assertUIObjectIsGone(itemWithText(tabUrl)) } } fun clickSuggestedLoginsButton() { for (i in 1..RETRY_COUNT) { try { - mDevice.waitForObjects(suggestedLogins) - suggestedLogins.click() - mDevice.waitForObjects(suggestedLogins) + mDevice.waitForObjects(suggestedLogins()) + suggestedLogins().click() + mDevice.waitForObjects(suggestedLogins()) break } catch (e: UiObjectNotFoundException) { if (i == RETRY_COUNT) { @@ -407,8 +385,8 @@ class BrowserRobot { fun clickSelectAddressButton() { for (i in 1..RETRY_COUNT) { try { - assertTrue(selectAddressButton.waitForExists(waitingTime)) - selectAddressButton.clickAndWaitForNewWindow(waitingTime) + assertUIObjectExists(selectAddressButton()) + selectAddressButton().clickAndWaitForNewWindow(waitingTime) break } catch (e: AssertionError) { @@ -424,7 +402,7 @@ class BrowserRobot { } } - fun verifySelectAddressButtonExists(exists: Boolean) = assertItemWithResIdExists(selectAddressButton, exists = exists) + fun verifySelectAddressButtonExists(exists: Boolean) = assertUIObjectExists(selectAddressButton(), exists = exists) fun changeCreditCardExpiryDate(expiryDate: String) = itemWithResId("expiryMonthAndYear").setText(expiryDate) @@ -451,17 +429,22 @@ class BrowserRobot { } fun verifyUpdateOrSaveCreditCardPromptExists(exists: Boolean) = - assertItemWithResIdAndTextExists( + assertUIObjectExists( itemWithResId("$packageName:id/save_credit_card_header"), exists = exists, ) fun verifySelectCreditCardPromptExists(exists: Boolean) = - assertItemWithResIdExists(selectCreditCardButton, exists = exists) + assertUIObjectExists(selectCreditCardButton(), exists = exists) fun verifyCreditCardSuggestion(vararg creditCardNumbers: String) { for (creditCardNumber in creditCardNumbers) { - assertTrue(creditCardSuggestion(creditCardNumber).waitForExists(waitingTime)) + assertUIObjectExists( + itemWithResIdContainingText( + "$packageName:id/credit_card_number", + creditCardNumber, + ), + ) } } @@ -470,29 +453,17 @@ class BrowserRobot { UiSelector() .resourceId("$packageName:id/mozac_feature_login_multiselect_expand"), ).waitForExists(waitingTime) - - assertTrue( - mDevice.findObject(UiSelector().textContains(userName)).waitForExists(waitingTime), - ) + assertUIObjectExists(itemContainingText(userName)) } fun verifyPrefilledLoginCredentials(userName: String, password: String, credentialsArePrefilled: Boolean) { // Sometimes the assertion of the pre-filled logins fails so we are re-trying after refreshing the page for (i in 1..RETRY_COUNT) { try { - if (credentialsArePrefilled) { - mDevice.waitForObjects(itemWithResId("username")) - assertTrue(itemWithResId("username").text.equals(userName)) - - mDevice.waitForObjects(itemWithResId("password")) - assertTrue(itemWithResId("password").text.equals(password)) - } else { - mDevice.waitForObjects(itemWithResId("username")) - assertFalse(itemWithResId("username").text.equals(userName)) - - mDevice.waitForObjects(itemWithResId("password")) - assertFalse(itemWithResId("password").text.equals(password)) - } + mDevice.waitForObjects(itemWithResId("username")) + assertItemTextEquals(itemWithResId("username"), expectedText = userName, isEqual = credentialsArePrefilled) + mDevice.waitForObjects(itemWithResId("password")) + assertItemTextEquals(itemWithResId("password"), expectedText = password, isEqual = credentialsArePrefilled) break } catch (e: AssertionError) { @@ -515,26 +486,17 @@ class BrowserRobot { fun verifyAutofilledAddress(streetAddress: String) { mDevice.waitForObjects(itemWithResIdAndText("streetAddress", streetAddress)) - assertTrue( - itemWithResIdAndText("streetAddress", streetAddress) - .waitForExists(waitingTime), - ) + assertUIObjectExists(itemWithResIdAndText("streetAddress", streetAddress)) } fun verifyManuallyFilledAddress(apartment: String) { mDevice.waitForObjects(itemWithResIdAndText("apartment", apartment)) - assertTrue( - itemWithResIdAndText("apartment", apartment) - .waitForExists(waitingTime), - ) + assertUIObjectExists(itemWithResIdAndText("apartment", apartment)) } fun verifyAutofilledCreditCard(creditCardNumber: String) { mDevice.waitForObjects(itemWithResIdAndText("cardNumber", creditCardNumber)) - assertTrue( - itemWithResIdAndText("cardNumber", creditCardNumber) - .waitForExists(waitingTime), - ) + assertUIObjectExists(itemWithResIdAndText("cardNumber", creditCardNumber)) } fun verifyPrefilledPWALoginCredentials(userName: String, shortcutTitle: String) { @@ -543,9 +505,9 @@ class BrowserRobot { var currentTries = 0 while (currentTries++ < 3) { try { - assertTrue(itemWithResId("submit").waitForExists(waitingTime)) + assertUIObjectExists(itemWithResId("submit")) itemWithResId("submit").click() - assertTrue(itemWithResId("username").text.equals(userName)) + assertItemTextEquals(itemWithResId("username"), expectedText = userName) break } catch (e: AssertionError) { addToHomeScreen { @@ -555,12 +517,12 @@ class BrowserRobot { } fun verifySaveLoginPromptIsDisplayed() = - assertItemWithResIdExists( + assertUIObjectExists( itemWithResId("$packageName:id/feature_prompt_login_fragment"), ) fun verifySaveLoginPromptIsNotDisplayed() = - assertItemWithResIdExists( + assertUIObjectExists( itemWithResId("$packageName:id/feature_prompt_login_fragment"), exists = false, ) @@ -568,11 +530,7 @@ class BrowserRobot { fun verifyTrackingProtectionWebContent(state: String) { for (i in 1..RETRY_COUNT) { try { - assertTrue( - mDevice.findObject( - UiSelector().textContains(state), - ).waitForExists(waitingTimeLong), - ) + assertUIObjectExists(itemContainingText(state)) break } catch (e: AssertionError) { @@ -618,7 +576,7 @@ class BrowserRobot { } } - fun selectTime(hour: Int, minute: Int) = + fun selectTime(hour: Int, minute: Int): ViewInteraction = onView( isAssignableFrom(TimePicker::class.java), ).inRoot( @@ -626,14 +584,14 @@ class BrowserRobot { ).perform(PickerActions.setTime(hour, minute)) fun verifySelectedDate() { + val currentDate = LocalDate.now() + val currentDay = currentDate.dayOfMonth + val currentMonth = currentDate.month + val currentYear = currentDate.year + for (i in 1..RETRY_COUNT) { try { - assertTrue( - mDevice.findObject( - UiSelector() - .text("Selected date is: $currentDate"), - ).waitForExists(waitingTime), - ) + assertUIObjectExists(itemContainingText("Selected date is: $currentDate")) break } catch (e: AssertionError) { @@ -646,32 +604,21 @@ class BrowserRobot { } } - assertTrue( - mDevice.findObject( - UiSelector() - .text("Selected date is: $currentDate"), - ).waitForExists(waitingTime), - ) + assertUIObjectExists(itemContainingText("Selected date is: $currentDate")) } fun verifyNoDateIsSelected() { - assertFalse( - mDevice.findObject( - UiSelector() - .text("Selected date is: $currentDate"), - ).waitForExists(waitingTimeShort), + val currentDate = LocalDate.now() + assertUIObjectExists( + itemContainingText("Selected date is: $currentDate"), + exists = false, ) } fun verifySelectedTime(hour: Int, minute: Int) { for (i in 1..RETRY_COUNT) { try { - assertTrue( - mDevice.findObject( - UiSelector() - .text("Selected time is: $hour:$minute"), - ).waitForExists(waitingTime), - ) + assertUIObjectExists(itemContainingText("Selected time is: $hour:$minute")) break } catch (e: AssertionError) { @@ -685,24 +632,13 @@ class BrowserRobot { clickPageObject(itemWithResId("submitTime")) } } - - assertTrue( - mDevice.findObject( - UiSelector() - .text("Selected time is: $hour:$minute"), - ).waitForExists(waitingTime), - ) + assertUIObjectExists(itemContainingText("Selected time is: $hour:$minute")) } fun verifySelectedColor(hexValue: String) { for (i in 1..RETRY_COUNT) { try { - assertTrue( - mDevice.findObject( - UiSelector() - .text("Selected color is: $hexValue"), - ).waitForExists(waitingTime), - ) + assertUIObjectExists(itemContainingText("Selected color is: $hexValue")) break } catch (e: AssertionError) { @@ -715,12 +651,7 @@ class BrowserRobot { } } - assertTrue( - mDevice.findObject( - UiSelector() - .text("Selected color is: $hexValue"), - ).waitForExists(waitingTime), - ) + assertUIObjectExists(itemContainingText("Selected color is: $hexValue")) } fun verifySelectedDropDownOption(optionName: String) { @@ -732,12 +663,7 @@ class BrowserRobot { .resourceId("submitOption"), ).waitForExists(waitingTime) - assertTrue( - mDevice.findObject( - UiSelector() - .text("Selected option is: $optionName"), - ).waitForExists(waitingTime), - ) + assertUIObjectExists(itemContainingText("Selected option is: $optionName")) break } catch (e: AssertionError) { @@ -749,36 +675,19 @@ class BrowserRobot { } } - assertTrue( - mDevice.findObject( - UiSelector() - .text("Selected option is: $optionName"), - ).waitForExists(waitingTime), - ) + assertUIObjectExists(itemContainingText("Selected option is: $optionName")) } - fun verifyNoTimeIsSelected(hour: Int, minute: Int) { - assertFalse( - mDevice.findObject( - UiSelector() - .text("Selected date is: $hour:$minute"), - ).waitForExists(waitingTimeShort), - ) - } + fun verifyNoTimeIsSelected(hour: Int, minute: Int) = + assertUIObjectExists(itemContainingText("Selected date is: $hour:$minute"), exists = false) - fun verifyColorIsNotSelected(hexValue: String) { - assertFalse( - mDevice.findObject( - UiSelector() - .text("Selected date is: $hexValue"), - ).waitForExists(waitingTimeShort), - ) - } + fun verifyColorIsNotSelected(hexValue: String) = + assertUIObjectExists(itemContainingText("Selected date is: $hexValue"), exists = false) fun verifyCookieBannerExists(exists: Boolean) { for (i in 1..RETRY_COUNT) { try { - assertItemWithResIdExists(cookieBanner, exists = exists) + assertUIObjectExists(cookieBanner(), exists = exists) break } catch (e: AssertionError) { if (i == RETRY_COUNT) { @@ -792,12 +701,12 @@ class BrowserRobot { } } } - assertItemWithResIdExists(cookieBanner, exists = exists) + assertUIObjectExists(cookieBanner(), exists = exists) } fun verifyOpenLinkInAnotherAppPrompt() { - assertItemWithResIdExists(itemWithResId("$packageName:id/parentPanel")) - assertItemContainingTextExists( + assertUIObjectExists( + itemWithResId("$packageName:id/parentPanel"), itemContainingText( getStringResource(R.string.mozac_feature_applinks_normal_confirm_dialog_title), ), @@ -810,7 +719,7 @@ class BrowserRobot { fun verifyPrivateBrowsingOpenLinkInAnotherAppPrompt(url: String, pageObject: UiObject) { for (i in 1..RETRY_COUNT) { try { - assertItemContainingTextExists( + assertUIObjectExists( itemContainingText( getStringResource(R.string.mozac_feature_applinks_confirm_dialog_title), ), @@ -834,40 +743,34 @@ class BrowserRobot { } fun verifyFindInPageBar(exists: Boolean) = - assertItemWithResIdExists( + assertUIObjectExists( itemWithResId("$packageName:id/findInPageView"), exists = exists, ) - fun verifyConnectionErrorMessage() { - assertItemContainingTextExists( + fun verifyConnectionErrorMessage() = + assertUIObjectExists( itemContainingText(getStringResource(R.string.mozac_browser_errorpages_connection_failure_title)), + itemWithResId("errorTryAgain"), ) - assertItemWithResIdExists(itemWithResId("errorTryAgain")) - } - fun verifyAddressNotFoundErrorMessage() { - assertItemContainingTextExists( + fun verifyAddressNotFoundErrorMessage() = + assertUIObjectExists( itemContainingText(getStringResource(R.string.mozac_browser_errorpages_unknown_host_title)), + itemWithResId("errorTryAgain"), ) - assertItemWithResIdExists(itemWithResId("errorTryAgain")) - } - fun verifyNoInternetConnectionErrorMessage() { - assertItemContainingTextExists( + fun verifyNoInternetConnectionErrorMessage() = + assertUIObjectExists( itemContainingText(getStringResource(R.string.mozac_browser_errorpages_no_internet_title)), + itemWithResId("errorTryAgain"), ) - assertItemWithResIdExists(itemWithResId("errorTryAgain")) - } fun verifyOpenLinksInAppsCFRExists(exists: Boolean) { for (i in 1..RETRY_COUNT) { try { - assertItemWithResIdExists( + assertUIObjectExists( itemWithResId("$packageName:id/banner_container"), - exists = exists, - ) - assertItemWithResIdAndTextExists( itemWithResIdContainingText( "$packageName:id/banner_info_message", getStringResource(R.string.open_in_app_cfr_info_message_2), @@ -889,54 +792,25 @@ class BrowserRobot { browserScreen { }.openThreeDotMenu { }.refreshPage { - progressBar.waitUntilGone(waitingTimeLong) + waitForPageToLoad() } } } } } - fun verifySurveyButton() { - val button = mDevice.findObject( - UiSelector().text( - getStringResource( - R.string.preferences_take_survey, - ), - ), - ) - assertTrue(button.waitForExists(waitingTime)) - } + fun verifySurveyButton() = assertUIObjectExists(itemContainingText(getStringResource(R.string.preferences_take_survey))) - fun verifySurveyButtonDoesNotExist() { - val button = mDevice.findObject( - UiSelector().text( - getStringResource( - R.string.preferences_take_survey, - ), - ), - ) - assertTrue(button.waitUntilGone(waitingTime)) - } + fun verifySurveyButtonDoesNotExist() = + assertUIObjectIsGone(itemWithText(getStringResource(R.string.preferences_take_survey))) - fun verifySurveyNoThanksButton() { - val button = mDevice.findObject( - UiSelector().text( - getStringResource( - R.string.preferences_not_take_survey, - ), - ), + fun verifySurveyNoThanksButton() = + assertUIObjectExists( + itemContainingText(getStringResource(R.string.preferences_not_take_survey)), ) - assertTrue(button.waitForExists(waitingTime)) - } - fun verifyHomeScreenSurveyCloseButton() { - val button = mDevice.findObject( - UiSelector().descriptionContains( - "Close", - ), - ) - assertTrue(button.waitForExists(waitingTime)) - } + fun verifyHomeScreenSurveyCloseButton() = + assertUIObjectExists(itemWithDescription("Close")) fun clickOpenLinksInAppsDismissCFRButton() = itemWithResIdContainingText( @@ -970,13 +844,13 @@ class BrowserRobot { fun longClickToolbar() = mDevice.findObject(By.res("$packageName:id/mozac_browser_toolbar_url_view")).click(LONG_CLICK_DURATION) fun verifyDownloadPromptIsDismissed() = - assertItemWithResIdExists( + assertUIObjectExists( itemWithResId("$packageName:id/viewDynamicDownloadDialog"), exists = false, ) fun verifyCancelPrivateDownloadsPrompt(numberOfActiveDownloads: String) { - assertItemWithResIdAndTextExists( + assertUIObjectExists( itemWithResIdContainingText( "$packageName:id/title", getStringResource(R.string.mozac_feature_downloads_cancel_active_downloads_warning_content_title), @@ -996,17 +870,20 @@ class BrowserRobot { ) } - fun clickStayInPrivateBrowsingPromptButton() = + fun clickStayInPrivateBrowsingPromptButton() { itemWithResIdContainingText( "$packageName:id/deny_button", getStringResource(R.string.mozac_feature_downloads_cancel_active_private_downloads_deny), ).click() + Log.i(TAG, "clickStayInPrivateBrowsingPromptButton: Clicked \"STAY IN PRIVATE BROWSING\" prompt button") + } fun clickCancelPrivateDownloadsPromptButton() { itemWithResIdContainingText( "$packageName:id/accept_button", getStringResource(R.string.mozac_feature_downloads_cancel_active_downloads_accept), ).click() + Log.i(TAG, "clickCancelPrivateDownloadsPromptButton: Clicked \"CANCEL DOWNLOADS\" prompt button") mDevice.waitForWindowUpdate(packageName, waitingTime) } @@ -1014,6 +891,7 @@ class BrowserRobot { fun fillPdfForm(name: String) { // Set PDF form text for the text box itemWithResId("pdfjs_internal_id_10R").setText(name) + Log.i(TAG, "fillPdfForm: Set PDF form text box text to: $name") mDevice.waitForWindowUpdate(packageName, waitingTime) if ( !itemWithResId("pdfjs_internal_id_11R").exists() && @@ -1023,15 +901,19 @@ class BrowserRobot { ) { // Close the keyboard mDevice.pressBack() + Log.i(TAG, "fillPdfForm: Closing the keyboard using device back button") } // Click PDF form check box itemWithResId("pdfjs_internal_id_11R").click() + Log.i(TAG, "fillPdfForm: Clicked PDF form check box") } class Transition { fun openThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition { mDevice.waitForIdle(waitingTime) + Log.i(TAG, "openThreeDotMenu: Device was idle for $waitingTime ms") threeDotButton().perform(click()) + Log.i(TAG, "openThreeDotMenu: Clicked the main menu button") ThreeDotMenuMainRobot().interact() return ThreeDotMenuMainRobot.Transition() @@ -1057,10 +939,7 @@ class BrowserRobot { ) tabsCounter().click() - assertTrue( - itemWithResId("$packageName:id/new_tab_button") - .waitForExists(waitingTime), - ) + assertUIObjectExists(itemWithResId("$packageName:id/new_tab_button")) break } catch (e: AssertionError) { @@ -1072,10 +951,7 @@ class BrowserRobot { } } - assertTrue( - itemWithResId("$packageName:id/new_tab_button") - .waitForExists(waitingTime), - ) + assertUIObjectExists(itemWithResId("$packageName:id/new_tab_button")) TabDrawerRobot().interact() return TabDrawerRobot.Transition() @@ -1114,6 +990,7 @@ class BrowserRobot { fun openNotificationShade(interact: NotificationRobot.() -> Unit): NotificationRobot.Transition { mDevice.openNotification() + Log.i(TAG, "openNotificationShade: Opened notification tray") NotificationRobot().interact() return NotificationRobot.Transition() @@ -1288,23 +1165,24 @@ class BrowserRobot { } fun clickSurveyButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { - surveyButton.waitForExists(waitingTime) - surveyButton.click() + surveyButton().waitForExists(waitingTime) + surveyButton().click() BrowserRobot().interact() return Transition() } fun clickNoThanksSurveyButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { - surveyNoThanksButton.waitForExists(waitingTime) - surveyNoThanksButton.click() + surveyNoThanksButton().waitForExists(waitingTime) + surveyNoThanksButton().click() BrowserRobot().interact() return Transition() } + fun clickHomeScreenSurveyCloseButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { - homescreenSurveyCloseButton.waitForExists(waitingTime) - homescreenSurveyCloseButton.click() + homescreenSurveyCloseButton().waitForExists(waitingTime) + homescreenSurveyCloseButton().click() BrowserRobot().interact() return Transition() @@ -1317,9 +1195,9 @@ fun browserScreen(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { return BrowserRobot.Transition() } -fun navURLBar() = itemWithResId("$packageName:id/toolbar") +private fun navURLBar() = itemWithResId("$packageName:id/toolbar") -fun searchBar() = itemWithResId("$packageName:id/mozac_browser_toolbar_url_view") +private fun searchBar() = itemWithResId("$packageName:id/mozac_browser_toolbar_url_view") fun homeScreenButton() = onView(withContentDescription(R.string.browser_toolbar_home)) @@ -1328,38 +1206,38 @@ private fun threeDotButton() = onView(withContentDescription("Menu")) private fun tabsCounter() = mDevice.findObject(By.res("$packageName:id/counter_root")) -private val progressBar = +private fun progressBar() = itemWithResId("$packageName:id/mozac_browser_toolbar_progress") -private val suggestedLogins = itemWithResId("$packageName:id/loginSelectBar") -private val selectAddressButton = itemWithResId("$packageName:id/select_address_header") -private val selectCreditCardButton = itemWithResId("$packageName:id/select_credit_card_header") - -private fun creditCardSuggestion(creditCardNumber: String) = - mDevice.findObject( - UiSelector() - .resourceId("$packageName:id/credit_card_number") - .textContains(creditCardNumber), - ) +private fun suggestedLogins() = itemWithResId("$packageName:id/loginSelectBar") +private fun selectAddressButton() = itemWithResId("$packageName:id/select_address_header") +private fun selectCreditCardButton() = itemWithResId("$packageName:id/select_credit_card_header") private fun siteSecurityToolbarButton() = itemWithResId("$packageName:id/mozac_browser_toolbar_security_indicator") fun clickPageObject(item: UiObject) { for (i in 1..RETRY_COUNT) { + Log.i(TAG, "clickPageObject: For loop i = $i") try { + Log.i(TAG, "clickPageObject: Try block") item.waitForExists(waitingTime) item.click() + Log.i(TAG, "clickPageObject: Clicked ${item.selector}") break } catch (e: UiObjectNotFoundException) { + Log.i(TAG, "clickPageObject: Catch block") if (i == RETRY_COUNT) { throw e } else { browserScreen { + Log.i(TAG, "clickPageObject: Browser screen") }.openThreeDotMenu { + Log.i(TAG, "clickPageObject: Opened main menu") }.refreshPage { - progressBar.waitUntilGone(waitingTime) + waitForPageToLoad() + Log.i(TAG, "clickPageObject: Page refreshed, progress bar is gone") } } } @@ -1380,7 +1258,7 @@ fun longClickPageObject(item: UiObject) { browserScreen { }.openThreeDotMenu { }.refreshPage { - progressBar.waitUntilGone(waitingTime) + waitForPageToLoad() } } } @@ -1412,7 +1290,7 @@ fun setPageObjectText(webPageItem: UiObject, text: String) { browserScreen { }.openThreeDotMenu { }.refreshPage { - progressBar.waitUntilGone(waitingTime) + waitForPageToLoad() } } } @@ -1424,11 +1302,7 @@ fun clearTextFieldItem(item: UiObject) { item.clearTextField() } -private val currentDate = LocalDate.now() -private val currentDay = currentDate.dayOfMonth -private val currentMonth = currentDate.month -private val currentYear = currentDate.year -private val cookieBanner = itemWithResId("startsiden-gdpr-disclaimer") +private fun cookieBanner() = itemWithResId("startsiden-gdpr-disclaimer") // Context menu items // Link URL @@ -1436,34 +1310,34 @@ private fun contextMenuLinkUrl(linkUrl: String) = itemContainingText(linkUrl) // Open link in new tab option -private val contextMenuOpenLinkInNewTab = +private fun contextMenuOpenLinkInNewTab() = itemContainingText(getStringResource(R.string.mozac_feature_contextmenu_open_link_in_new_tab)) // Open link in private tab option -private val contextMenuOpenLinkInPrivateTab = +private fun contextMenuOpenLinkInPrivateTab() = itemContainingText(getStringResource(R.string.mozac_feature_contextmenu_open_link_in_private_tab)) // Copy link option -private val contextMenuCopyLink = +private fun contextMenuCopyLink() = itemContainingText(getStringResource(R.string.mozac_feature_contextmenu_copy_link)) // Download link option -private val contextMenuDownloadLink = +private fun contextMenuDownloadLink() = itemContainingText(getStringResource(R.string.mozac_feature_contextmenu_download_link)) // Share link option -private val contextMenuShareLink = +private fun contextMenuShareLink() = itemContainingText(getStringResource(R.string.mozac_feature_contextmenu_share_link)) // Open in external app option -private val contextMenuOpenInExternalApp = +private fun contextMenuOpenInExternalApp() = itemContainingText(getStringResource(R.string.mozac_feature_contextmenu_open_link_in_external_app)) -private val surveyButton = +private fun surveyButton() = itemContainingText(getStringResource(R.string.preferences_take_survey)) -private val surveyNoThanksButton = +private fun surveyNoThanksButton() = itemContainingText(getStringResource(R.string.preferences_not_take_survey)) -private val homescreenSurveyCloseButton = +private fun homescreenSurveyCloseButton() = itemWithDescription("Close") diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/CollectionRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/CollectionRobot.kt index 73f981d9a5..1cbac1730e 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/CollectionRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/CollectionRobot.kt @@ -7,7 +7,6 @@ package org.mozilla.fenix.ui.robots import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasText -import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTouchInput @@ -21,17 +20,17 @@ import androidx.test.uiautomator.By import androidx.test.uiautomator.UiScrollable import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.Until -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue import org.mozilla.fenix.R -import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithDescriptionExists +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource +import org.mozilla.fenix.helpers.MatcherHelper.assertItemTextEquals +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectIsGone import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId +import org.mozilla.fenix.helpers.MatcherHelper.itemWithText import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText @@ -40,28 +39,16 @@ import org.mozilla.fenix.helpers.ext.waitNotNull class CollectionRobot { - fun verifySelectCollectionScreen() { - assertTrue( - mDevice.findObject(UiSelector().text("Select collection")) - .exists(), + fun verifySelectCollectionScreen() = + assertUIObjectExists( + itemContainingText("Select collection"), + itemContainingText("Add new collection"), + itemWithResId("$packageName:id/collections_list"), ) - assertTrue( - mDevice.findObject(UiSelector().resourceId("$packageName:id/collections_list")) - .exists(), - ) - assertTrue( - mDevice.findObject(UiSelector().text("Add new collection")) - .exists(), - ) - } fun clickAddNewCollection() = addNewCollectionButton().click() - fun verifyCollectionNameTextField() { - assertTrue( - mainMenuEditCollectionNameField().waitForExists(waitingTime), - ) - } + fun verifyCollectionNameTextField() = assertUIObjectExists(mainMenuEditCollectionNameField()) // names a collection saved from tab drawer fun typeCollectionNameAndSave(collectionName: String) { @@ -71,13 +58,12 @@ class CollectionRobot { } fun verifyTabsSelectedCounterText(numOfTabs: Int) { - mDevice.findObject(UiSelector().text("Select tabs to save")) - .waitUntilGone(waitingTime) + itemWithText("Select tabs to save").waitUntilGone(waitingTime) val tabsCounter = mDevice.findObject(UiSelector().resourceId("$packageName:id/bottom_bar_text")) when (numOfTabs) { - 1 -> assertTrue(tabsCounter.text.equals("$numOfTabs tab selected")) - 2 -> assertTrue(tabsCounter.text.equals("$numOfTabs tabs selected")) + 1 -> assertItemTextEquals(tabsCounter, expectedText = "$numOfTabs tab selected") + 2 -> assertItemTextEquals(tabsCounter, expectedText = "$numOfTabs tabs selected") } } @@ -88,33 +74,17 @@ class CollectionRobot { fun verifyTabSavedInCollection(title: String, visible: Boolean = true) { if (visible) { scrollToElementByText(title) - assertTrue( - collectionListItem(title).waitForExists(waitingTime), - ) + assertUIObjectExists(collectionListItem(title)) } else { - assertTrue( - collectionListItem(title).waitUntilGone(waitingTime), - ) + assertUIObjectIsGone(collectionListItem(title)) } } - fun verifyCollectionTabUrl(visible: Boolean, url: String) { - val tabUrl = mDevice.findObject(UiSelector().text(url)) - - if (visible) { - assertTrue(tabUrl.exists()) - } else { - assertFalse(tabUrl.exists()) - } - } + fun verifyCollectionTabUrl(visible: Boolean, url: String) = + assertUIObjectExists(itemContainingText(url), exists = visible) - fun verifyShareCollectionButtonIsVisible(visible: Boolean) { - if (visible) { - assertTrue(shareCollectionButton().exists()) - } else { - assertFalse(shareCollectionButton().exists()) - } - } + fun verifyShareCollectionButtonIsVisible(visible: Boolean) = + assertUIObjectExists(shareCollectionButton(), exists = visible) fun verifyCollectionMenuIsVisible(visible: Boolean, rule: ComposeTestRule) { if (visible) { @@ -160,17 +130,8 @@ class CollectionRobot { .performClick() } - fun verifyCollectionItemRemoveButtonIsVisible(title: String, visible: Boolean) { - if (visible) { - assertTrue( - removeTabFromCollectionButton(title).exists(), - ) - } else { - assertFalse( - removeTabFromCollectionButton(title).exists(), - ) - } - } + fun verifyCollectionItemRemoveButtonIsVisible(title: String, visible: Boolean) = + assertUIObjectExists(removeTabFromCollectionButton(title), exists = visible) fun removeTabFromCollection(title: String) = removeTabFromCollectionButton(title).click() @@ -202,9 +163,9 @@ class CollectionRobot { title: String, interact: HomeScreenRobot.() -> Unit, ): HomeScreenRobot.Transition { - assertItemContainingTextExists(itemContainingText(title)) + assertUIObjectExists(itemContainingText(title)) itemContainingText(title).clickAndWaitForNewWindow(waitingTimeShort) - assertItemWithDescriptionExists(itemWithDescription(getStringResource(R.string.remove_tab_from_collection)), exists = false) + assertUIObjectExists(itemWithDescription(getStringResource(R.string.remove_tab_from_collection)), exists = false) HomeScreenRobot().interact() return HomeScreenRobot.Transition() diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ComposeTabDrawerRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ComposeTabDrawerRobot.kt index e163d8f74d..baa7271b07 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ComposeTabDrawerRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ComposeTabDrawerRobot.kt @@ -39,16 +39,15 @@ import androidx.test.espresso.action.GeneralLocation import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers import com.google.android.material.bottomsheet.BottomSheetBehavior -import junit.framework.TestCase import org.hamcrest.Matcher import org.mozilla.fenix.R import org.mozilla.fenix.helpers.Constants +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.HomeActivityComposeTestRule -import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.clickAtLocationInView import org.mozilla.fenix.helpers.idlingresource.BottomSheetBehaviorStateIdlingResource @@ -87,7 +86,7 @@ class ComposeTabDrawerRobot(private val composeTestRule: HomeActivityComposeTest fun verifySyncedTabsListWhenUserIsNotSignedIn() { verifySyncedTabsList() - assertItemContainingTextExists( + assertUIObjectExists( itemContainingText(getStringResource(R.string.synced_tabs_sign_in_message)), itemContainingText(getStringResource(R.string.sync_sign_in)), itemContainingText(getStringResource(R.string.tab_drawer_fab_sync)), @@ -109,8 +108,9 @@ class ComposeTabDrawerRobot(private val composeTestRule: HomeActivityComposeTest fun verifyNoExistingOpenTabs(vararg titles: String) { titles.forEach { title -> - TestCase.assertFalse( - itemContainingText(title).waitForExists(TestAssetHelper.waitingTimeShort), + assertUIObjectExists( + itemContainingText(title), + exists = false, ) } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/CustomTabRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/CustomTabRobot.kt index c7a303937b..fd76071c1c 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/CustomTabRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/CustomTabRobot.kt @@ -12,19 +12,17 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.uiautomator.By import androidx.test.uiautomator.UiSelector -import junit.framework.TestCase.assertTrue import org.mozilla.fenix.R import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithDescriptionExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndTextExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestHelper.appName -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.TestHelper.waitForObjects @@ -36,16 +34,16 @@ import org.mozilla.fenix.helpers.click class CustomTabRobot { fun verifyCustomTabsSiteInfoButton() = - assertItemWithResIdExists( + assertUIObjectExists( itemWithResId("$packageName:id/mozac_browser_toolbar_security_indicator"), ) fun verifyCustomTabsShareButton() = - assertItemWithDescriptionExists( + assertUIObjectExists( itemWithDescription(getStringResource(R.string.mozac_feature_customtabs_share_link)), ) - fun verifyMainMenuButton() = assertItemWithResIdExists(mainMenuButton) + fun verifyMainMenuButton() = assertUIObjectExists(mainMenuButton) fun verifyDesktopSiteButtonExists() { desktopSiteButton().check(matches(isDisplayed())) @@ -55,26 +53,20 @@ class CustomTabRobot { findInPageButton().check(matches(isDisplayed())) } - fun verifyPoweredByTextIsDisplayed() { - assertTrue( - mDevice.findObject(UiSelector().textContains("POWERED BY $appName")) - .waitForExists(waitingTime), - ) - } + fun verifyPoweredByTextIsDisplayed() = + assertUIObjectExists(itemContainingText("POWERED BY $appName")) fun verifyOpenInBrowserButtonExists() { openInBrowserButton().check(matches(isDisplayed())) } - fun verifyBackButtonExists() = assertTrue(backButton().waitForExists(waitingTime)) + fun verifyBackButtonExists() = assertUIObjectExists(itemWithDescription("Back")) - fun verifyForwardButtonExists() = assertTrue(forwardButton().waitForExists(waitingTime)) + fun verifyForwardButtonExists() = assertUIObjectExists(itemWithDescription("Forward")) - fun verifyRefreshButtonExists() = assertTrue(refreshButton().waitForExists(waitingTime)) + fun verifyRefreshButtonExists() = assertUIObjectExists(itemWithDescription("Refresh")) - fun verifyCustomMenuItem(label: String) { - assertTrue(mDevice.findObject(UiSelector().text(label)).waitForExists(waitingTime)) - } + fun verifyCustomMenuItem(label: String) = assertUIObjectExists(itemContainingText(label)) fun verifyCustomTabCloseButton() { closeButton().check(matches(isDisplayed())) @@ -95,17 +87,13 @@ class CustomTabRobot { waitingTime, ) - assertTrue( - mDevice.findObject( - UiSelector() - .resourceId("$packageName:id/mozac_browser_toolbar_title_view") - .textContains(title), - ).waitForExists(waitingTime), + assertUIObjectExists( + itemWithResIdContainingText("$packageName:id/mozac_browser_toolbar_title_view", title), ) } fun verifyCustomTabUrl(Url: String) { - assertItemWithResIdAndTextExists( + assertUIObjectExists( itemWithResIdContainingText("$packageName:id/mozac_browser_toolbar_url_view", Url.drop(7)), ) } @@ -135,10 +123,10 @@ class CustomTabRobot { fun clickCustomTabCloseButton() = closeButton().click() fun verifyCustomTabActionButton(customTabActionButtonDescription: String) = - assertItemWithDescriptionExists(itemWithDescription(customTabActionButtonDescription)) + assertUIObjectExists(itemWithDescription(customTabActionButtonDescription)) fun verifyPDFReaderToolbarItems() = - assertItemWithResIdAndTextExists( + assertUIObjectExists( itemWithResIdAndText("download", "Download"), itemWithResIdAndText("openInApp", "Open in app"), ) @@ -183,12 +171,6 @@ private fun findInPageButton() = onView(withText("Find in page")) private fun openInBrowserButton() = onView(withText("Open in $appName")) -private fun refreshButton() = mDevice.findObject(UiSelector().description("Refresh")) - -private fun forwardButton() = mDevice.findObject(UiSelector().description("Forward")) - -private fun backButton() = mDevice.findObject(UiSelector().description("Back")) - private fun closeButton() = onView(withContentDescription("Return to previous app")) private fun customTabToolbar() = mDevice.findObject(By.res("$packageName:id/toolbar")) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DownloadRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DownloadRobot.kt index 085723b6fe..975000dd20 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DownloadRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DownloadRobot.kt @@ -7,6 +7,7 @@ package org.mozilla.fenix.ui.robots import android.content.Intent +import android.net.Uri import android.util.Log import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click @@ -22,20 +23,20 @@ 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.CoreMatchers import org.hamcrest.CoreMatchers.allOf -import org.junit.Assert.assertTrue import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.AppAndSystemHelper.assertExternalAppOpens +import org.mozilla.fenix.helpers.AppAndSystemHelper.getPermissionAllowID import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_APPS_PHOTOS +import org.mozilla.fenix.helpers.Constants.TAG +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime -import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeLong -import org.mozilla.fenix.helpers.TestHelper -import org.mozilla.fenix.helpers.TestHelper.assertExternalAppOpens -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.click @@ -47,168 +48,181 @@ import org.mozilla.fenix.helpers.ext.waitNotNull class DownloadRobot { - fun verifyDownloadPrompt(fileName: String) = assertDownloadPrompt(fileName) - - fun verifyDownloadCompleteNotificationPopup() { - assertTrue( - "Download notification Open button not found", - mDevice.findObject(UiSelector().text("Open")) - .waitForExists(waitingTime), - ) - assertTrue( - "Download completed notification text doesn't match", - mDevice.findObject(UiSelector().textContains("Download completed")) - .waitForExists(waitingTime), - ) - assertTrue( - "Downloaded file name not visible", - mDevice.findObject(UiSelector().resourceId("$packageName:id/download_dialog_filename")) - .waitForExists(waitingTime), - ) + fun verifyDownloadPrompt(fileName: String) { + var currentTries = 0 + while (currentTries++ < 3) { + Log.i(TAG, "verifyDownloadPrompt: While loop currentTries = $currentTries") + try { + Log.i(TAG, "verifyDownloadPrompt: Try block") + assertUIObjectExists( + itemWithResId("$packageName:id/download_button"), + itemContainingText(fileName), + ) + + break + } catch (e: AssertionError) { + Log.i(TAG, "verifyDownloadPrompt: Catch block") + Log.e("DOWNLOAD_ROBOT", "Failed to find locator: ${e.localizedMessage}") + + browserScreen { + }.clickDownloadLink(fileName) { + } + } + } } - fun verifyDownloadFailedPrompt(fileName: String) { - assertTrue( - itemWithResId("$packageName:id/download_dialog_icon") - .waitForExists(waitingTime), + fun verifyDownloadCompleteNotificationPopup() = + assertUIObjectExists( + itemContainingText(getStringResource(R.string.mozac_feature_downloads_button_open)), + itemContainingText(getStringResource(R.string.mozac_feature_downloads_completed_notification_text2)), + itemWithResId("$packageName:id/download_dialog_filename"), ) - assertTrue( - "Download dialog title not displayed", - itemWithResIdAndText( + + fun verifyDownloadFailedPrompt(fileName: String) = + assertUIObjectExists( + itemWithResId("$packageName:id/download_dialog_icon"), + itemWithResIdContainingText( "$packageName:id/download_dialog_title", - "Download failed", - ).waitForExists(waitingTime), - ) - assertTrue( - "Download file name not displayed", + getStringResource(R.string.mozac_feature_downloads_failed_notification_text2), + ), itemWithResIdContainingText( "$packageName:id/download_dialog_filename", fileName, - ).waitForExists(waitingTime), - ) - assertTrue( - "Try again button not displayed", - itemWithResIdAndText( + ), + itemWithResIdContainingText( "$packageName:id/download_dialog_action_button", - "Try Again", - ).waitForExists(waitingTime), + getStringResource(R.string.mozac_feature_downloads_button_try_again), + ), ) - } fun clickTryAgainButton() { itemWithResIdAndText( "$packageName:id/download_dialog_action_button", "Try Again", ).click() + Log.i(TAG, "clickTryAgainButton: Clicked \"TRY AGAIN\" in app prompt button") } fun verifyPhotosAppOpens() = assertExternalAppOpens(GOOGLE_APPS_PHOTOS) - fun verifyDownloadedFileName(fileName: String) { - assertTrue( - "$fileName not found in Downloads list", - mDevice.findObject(UiSelector().text(fileName)) - .waitForExists(waitingTime), - ) - } + fun verifyDownloadedFileName(fileName: String) = + assertUIObjectExists(itemContainingText(fileName)) - fun verifyDownloadedFileIcon() = assertDownloadedFileIcon() + fun verifyDownloadedFileIcon() = assertUIObjectExists(itemWithResId("$packageName:id/favicon")) fun verifyEmptyDownloadsList() { + Log.i(TAG, "verifyEmptyDownloadsList: Looking for empty download list") mDevice.findObject(UiSelector().resourceId("$packageName:id/download_empty_view")) .waitForExists(waitingTime) onView(withText("No downloaded files")).check(matches(isDisplayed())) + Log.i(TAG, "verifyEmptyDownloadsList: Verified \"No downloaded files\" list message") } fun waitForDownloadsListToExist() = - assertTrue( - "Downloads list either empty or not displayed", - mDevice.findObject(UiSelector().resourceId("$packageName:id/download_list")) - .waitForExists(waitingTime), - ) + assertUIObjectExists(itemWithResId("$packageName:id/download_list")) fun openDownloadedFile(fileName: String) { downloadedFile(fileName) .check(matches(isDisplayed())) .click() + Log.i(TAG, "openDownloadedFile: Clicked downloaded file: $fileName") } - fun deleteDownloadedItem(fileName: String) = + fun deleteDownloadedItem(fileName: String) { onView( allOf( withId(R.id.overflow_menu), hasSibling(withText(fileName)), ), ).click() + Log.i(TAG, "deleteDownloadedItem: Deleted downloaded file: $fileName using trash bin icon") + } - fun longClickDownloadedItem(title: String) = + fun longClickDownloadedItem(title: String) { onView( allOf( withId(R.id.title), withText(title), ), ).perform(longClick()) + Log.i(TAG, "longClickDownloadedItem: Long clicked downloaded file: $title") + } - fun selectDownloadedItem(title: String) = + fun selectDownloadedItem(title: String) { onView( allOf( withId(R.id.title), withText(title), ), ).perform(click()) + Log.i(TAG, "selectDownloadedItem: Selected downloaded file: $title") + } - fun openMultiSelectMoreOptionsMenu() = + fun openMultiSelectMoreOptionsMenu() { itemWithDescription(getStringResource(R.string.content_description_menu)).click() + Log.i(TAG, "openMultiSelectMoreOptionsMenu: Clicked multi-select more options button") + } - fun clickMultiSelectRemoveButton() = + fun clickMultiSelectRemoveButton() { itemWithResIdContainingText("$packageName:id/title", "Remove").click() + Log.i(TAG, "clickMultiSelectRemoveButton: Clicked multi-select remove button") + } + + fun openPageAndDownloadFile(url: Uri, downloadFile: String) { + navigationToolbar { + }.enterURLAndEnterToBrowser(url) { + waitForPageToLoad() + }.clickDownloadLink(downloadFile) { + verifyDownloadPrompt(downloadFile) + }.clickDownload { + } + } class Transition { fun clickDownload(interact: DownloadRobot.() -> Unit): Transition { downloadButton().click() + Log.i(TAG, "clickDownload: Clicked \"DOWNLOAD\" button from prompt") DownloadRobot().interact() return Transition() } - fun closeCompletedDownloadPrompt(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { - closeCompletedDownloadButton().click() - - BrowserRobot().interact() - return BrowserRobot.Transition() - } - fun closeDownloadPrompt(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { itemWithResId("$packageName:id/download_dialog_close_button").click() + Log.i(TAG, "closeDownloadPrompt: Dismissed download prompt by clicking close prompt button") BrowserRobot().interact() return BrowserRobot.Transition() } fun clickOpen(type: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + Log.i(TAG, "clickOpen: Looking for \"OPEN\" download prompt button") openDownloadButton().waitForExists(waitingTime) openDownloadButton().click() + Log.i(TAG, "clickOpen: Clicked \"OPEN\" download prompt button") // verify open intent is matched with associated data type Intents.intended( - CoreMatchers.allOf( + allOf( IntentMatchers.hasAction(Intent.ACTION_VIEW), IntentMatchers.hasType(type), ), ) + Log.i(TAG, "clickOpen: Verified that open intent is matched with associated data type") BrowserRobot().interact() return BrowserRobot.Transition() } fun clickAllowPermission(interact: DownloadRobot.() -> Unit): Transition { + Log.i(TAG, "clickAllowPermission: Looking for \"ALLOW\" permission button") mDevice.waitNotNull( - Until.findObject(By.res(TestHelper.getPermissionAllowID() + ":id/permission_allow_button")), + Until.findObject(By.res(getPermissionAllowID() + ":id/permission_allow_button")), waitingTime, ) - val allowPermissionButton = mDevice.findObject(By.res(TestHelper.getPermissionAllowID() + ":id/permission_allow_button")) - allowPermissionButton.click() + mDevice.findObject(By.res(getPermissionAllowID() + ":id/permission_allow_button")).click() + Log.i(TAG, "clickAllowPermission: Clicked \"ALLOW\" permission button") DownloadRobot().interact() return Transition() @@ -216,6 +230,7 @@ class DownloadRobot { fun exitDownloadsManagerToBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { onView(withContentDescription("Navigate up")).click() + Log.i(TAG, "exitDownloadsManagerToBrowser: Exited download manager to browser by clicking the navigate up toolbar button") BrowserRobot().interact() return BrowserRobot.Transition() @@ -223,6 +238,7 @@ class DownloadRobot { fun goBack(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { goBackButton().click() + Log.i(TAG, "exitDownloadsManagerToBrowser: Exited download manager to home screen by clicking the navigate up toolbar button") HomeScreenRobot().interact() return HomeScreenRobot.Transition() @@ -235,38 +251,6 @@ fun downloadRobot(interact: DownloadRobot.() -> Unit): DownloadRobot.Transition return DownloadRobot.Transition() } -private fun assertDownloadPrompt(fileName: String) { - var currentTries = 0 - while (currentTries++ < 3) { - try { - assertTrue( - "Download prompt button not visible", - mDevice.findObject(UiSelector().resourceId("$packageName:id/download_button")) - .waitForExists(waitingTimeLong), - ) - assertTrue( - "$fileName title doesn't match", - mDevice.findObject(UiSelector().text(fileName)) - .waitForExists(waitingTimeLong), - ) - - break - } catch (e: AssertionError) { - Log.e("DOWNLOAD_ROBOT", "Failed to find locator: ${e.localizedMessage}") - - browserScreen { - }.clickDownloadLink(fileName) { - } - } - } -} - -private fun closeCompletedDownloadButton() = - onView(withId(R.id.download_dialog_close_button)) - -private fun closePromptButton() = - onView(withId(R.id.close_button)) - private fun downloadButton() = onView(withId(R.id.download_button)) .check(matches(isDisplayed())) @@ -276,11 +260,4 @@ private fun openDownloadButton() = private fun downloadedFile(fileName: String) = onView(withText(fileName)) -private fun assertDownloadedFileIcon() = - assertTrue( - "Downloaded file icon not found", - mDevice.findObject(UiSelector().resourceId("$packageName:id/favicon")) - .exists(), - ) - private fun goBackButton() = onView(withContentDescription("Navigate up")) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/EnhancedTrackingProtectionRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/EnhancedTrackingProtectionRobot.kt index dda03c2e12..6c7cdeff3a 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/EnhancedTrackingProtectionRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/EnhancedTrackingProtectionRobot.kt @@ -17,15 +17,15 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.uiautomator.By import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.Until -import junit.framework.TestCase.assertTrue import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.containsString import org.hamcrest.Matchers.not import org.mozilla.fenix.R -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId +import org.mozilla.fenix.helpers.MatcherHelper.itemWithText import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.click @@ -42,10 +42,7 @@ class EnhancedTrackingProtectionRobot { fun verifyETPSwitchVisibility(visible: Boolean) = assertETPSwitchVisibility(visible) fun verifyCrossSiteCookiesBlocked(isBlocked: Boolean) { - assertTrue( - mDevice.findObject(UiSelector().resourceId("$packageName:id/cross_site_tracking")) - .waitForExists(waitingTime), - ) + assertUIObjectExists(itemWithResId("$packageName:id/cross_site_tracking")) crossSiteCookiesBlockListButton.click() // Verifies the trackers block/allow list onView(withId(R.id.details_blocking_header)) @@ -63,10 +60,7 @@ class EnhancedTrackingProtectionRobot { } fun verifySocialMediaTrackersBlocked(isBlocked: Boolean) { - assertTrue( - mDevice.findObject(UiSelector().resourceId("$packageName:id/social_media_trackers")) - .waitForExists(waitingTime), - ) + assertUIObjectExists(itemWithResId("$packageName:id/social_media_trackers")) socialTrackersBlockListButton.click() // Verifies the trackers block/allow list onView(withId(R.id.details_blocking_header)) @@ -85,10 +79,7 @@ class EnhancedTrackingProtectionRobot { } fun verifyFingerprintersBlocked(isBlocked: Boolean) { - assertTrue( - mDevice.findObject(UiSelector().resourceId("$packageName:id/fingerprinters")) - .waitForExists(waitingTime), - ) + assertUIObjectExists(itemWithResId("$packageName:id/fingerprinters")) fingerprintersBlockListButton.click() // Verifies the trackers block/allow list onView(withId(R.id.details_blocking_header)) @@ -107,10 +98,7 @@ class EnhancedTrackingProtectionRobot { } fun verifyCryptominersBlocked(isBlocked: Boolean) { - assertTrue( - mDevice.findObject(UiSelector().resourceId("$packageName:id/cryptominers")) - .waitForExists(waitingTime), - ) + assertUIObjectExists(itemWithResId("$packageName:id/cryptominers")) cryptominersBlockListButton.click() // Verifies the trackers block/allow list onView(withId(R.id.details_blocking_header)) @@ -129,10 +117,7 @@ class EnhancedTrackingProtectionRobot { } fun verifyTrackingContentBlocked(isBlocked: Boolean) { - assertTrue( - mDevice.findObject(UiSelector().text("Tracking Content")) - .waitForExists(waitingTime), - ) + assertUIObjectExists(itemWithText("Tracking Content")) trackingContentBlockListButton.click() // Verifies the trackers block/allow list onView(withId(R.id.details_blocking_header)) @@ -166,7 +151,7 @@ class EnhancedTrackingProtectionRobot { } fun verifyETPSectionIsDisplayedInQuickSettingsSheet(isDisplayed: Boolean) = - assertItemWithResIdExists( + assertUIObjectExists( itemWithResId("$packageName:id/trackingProtectionLayout"), exists = isDisplayed, ) @@ -304,9 +289,5 @@ private val fingerprintersBlockListButton = private fun assertSecuritySheetIsCompletelyDisplayed() { mDevice.findObject(UiSelector().description(getStringResource(R.string.quick_settings_sheet))) .waitForExists(waitingTime) - assertTrue( - mDevice.findObject( - UiSelector().resourceId("$packageName:id/quick_action_sheet"), - ).waitForExists(waitingTime), - ) + assertUIObjectExists(itemWithResId("$packageName:id/quick_action_sheet")) } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HistoryRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HistoryRobot.kt index 0cd61c723f..768121d451 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HistoryRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HistoryRobot.kt @@ -21,13 +21,13 @@ import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.Until import org.hamcrest.Matchers import org.hamcrest.Matchers.allOf -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId +import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime -import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.click @@ -60,13 +60,8 @@ class HistoryRobot { assertVisitedTimeTitle() } - fun verifyHistoryItemExists(shouldExist: Boolean, item: String) { - if (shouldExist) { - assertTrue(mDevice.findObject(UiSelector().textContains(item)).waitForExists(waitingTime)) - } else { - assertFalse(mDevice.findObject(UiSelector().textContains(item)).waitForExists(waitingTimeShort)) - } - } + fun verifyHistoryItemExists(shouldExist: Boolean, item: String) = + assertUIObjectExists(itemContainingText(item), exists = shouldExist) fun verifyFirstTestPageTitle(title: String) = assertTestPageTitle(title) @@ -111,22 +106,15 @@ class HistoryRobot { snackBarUndoButton().click() } - fun verifySearchGroupDisplayed(shouldBeDisplayed: Boolean, searchTerm: String, groupSize: Int) { + fun verifySearchGroupDisplayed(shouldBeDisplayed: Boolean, searchTerm: String, groupSize: Int) = // checks if the search group exists in the Recently visited section - if (shouldBeDisplayed) { - assertTrue( - mDevice.findObject(UiSelector().text(searchTerm)) - .getFromParent(UiSelector().text("$groupSize sites")) - .waitForExists(waitingTimeShort), - ) - } else { - assertFalse( - mDevice.findObject(UiSelector().text(searchTerm)) - .getFromParent(UiSelector().text("$groupSize sites")) - .waitForExists(waitingTimeShort), - ) - } - } + assertUIObjectExists( + itemContainingText(searchTerm) + .getFromParent( + UiSelector().text("$groupSize sites"), + ), + exists = shouldBeDisplayed, + ) fun openSearchGroup(searchTerm: String) { mDevice.findObject(UiSelector().text(searchTerm)).waitForExists(waitingTime) @@ -208,10 +196,11 @@ private fun assertTestPageTitle(title: String) = testPageTitle() .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .check(matches(withText(title))) -private fun assertDeleteConfirmationMessage() { - assertTrue(deleteHistoryPromptTitle().waitForExists(waitingTime)) - assertTrue(deleteHistoryPromptSummary().waitForExists(waitingTime)) -} +private fun assertDeleteConfirmationMessage() = + assertUIObjectExists( + itemWithResIdContainingText("$packageName:id/title", getStringResource(R.string.delete_history_prompt_title)), + itemWithResIdContainingText("$packageName:id/body", getStringResource(R.string.delete_history_prompt_body_2)), + ) private fun assertCopySnackBarText() = snackBarText().check(matches(withText("URL copied"))) @@ -223,22 +212,6 @@ private fun snackBarUndoButton() = onView(withId(R.id.snackbar_btn)) private fun assertUndoDeleteSnackBarButton() = snackBarUndoButton().check(matches(withText("UNDO"))) -private fun deleteHistoryPromptTitle() = - mDevice - .findObject( - UiSelector() - .textContains(getStringResource(R.string.delete_history_prompt_title)) - .resourceId("$packageName:id/title"), - ) - -private fun deleteHistoryPromptSummary() = - mDevice - .findObject( - UiSelector() - .textContains(getStringResource(R.string.delete_history_prompt_body_2)) - .resourceId("$packageName:id/body"), - ) - private fun deleteHistoryEverythingOption() = mDevice .findObject( diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HomeScreenRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HomeScreenRobot.kt index 0b26c69f62..e5912965ec 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HomeScreenRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HomeScreenRobot.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.ui.robots +import android.util.Log import android.view.View import android.widget.EditText import android.widget.TextView @@ -47,25 +48,27 @@ import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.instanceOf import org.hamcrest.Matchers import org.junit.Assert -import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.mozilla.fenix.R import org.mozilla.fenix.helpers.Constants.LISTS_MAXSWIPES import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION +import org.mozilla.fenix.helpers.Constants.TAG +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.HomeActivityComposeTestRule -import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithDescriptionExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndTextExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectIsGone import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription +import org.mozilla.fenix.helpers.MatcherHelper.itemWithIndex import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndDescription +import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndIndex import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText +import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText +import org.mozilla.fenix.helpers.MatcherHelper.itemWithText import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort import org.mozilla.fenix.helpers.TestHelper.appName -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText @@ -83,55 +86,38 @@ class HomeScreenRobot { " service provider, it makes it easier to keep what you do online private from anyone" + " else who uses this device." - fun verifyNavigationToolbar() = assertItemWithResIdExists(navigationToolbar) + fun verifyNavigationToolbar() = assertUIObjectExists(navigationToolbar) - fun verifyHomeScreen() = assertItemWithResIdExists(homeScreen) + fun verifyHomeScreen() = assertUIObjectExists(homeScreen) fun verifyPrivateBrowsingHomeScreenItems() { verifyHomeScreenAppBarItems() - assertItemContainingTextExists(itemContainingText(privateSessionMessage)) + assertUIObjectExists(itemContainingText(privateSessionMessage)) verifyCommonMythsLink() } fun verifyHomeScreenAppBarItems() = - assertItemWithResIdExists(homeScreen, privateBrowsingButton, homepageWordmark) + assertUIObjectExists(homeScreen, privateBrowsingButton, homepageWordmark) - fun verifyNavigationToolbarItems(numberOfOpenTabs: String = "0") { - assertItemWithResIdExists(navigationToolbar, menuButton) - assertItemWithResIdAndTextExists(tabCounter(numberOfOpenTabs)) - } + fun verifyNavigationToolbarItems(numberOfOpenTabs: String = "0") = + assertUIObjectExists(navigationToolbar, menuButton, tabCounter(numberOfOpenTabs)) - fun verifyHomePrivateBrowsingButton() = assertItemWithResIdExists(privateBrowsingButton) - fun verifyHomeMenuButton() = assertItemWithResIdExists(menuButton) + fun verifyHomePrivateBrowsingButton() = assertUIObjectExists(privateBrowsingButton) + fun verifyHomeMenuButton() = assertUIObjectExists(menuButton) fun verifyTabButton() = assertTabButton() fun verifyCollectionsHeader() = assertCollectionsHeader() fun verifyNoCollectionsText() = assertNoCollectionsText() fun verifyHomeWordmark() { homeScreenList().scrollToBeginning(3) - assertItemWithResIdExists(homepageWordmark) + assertUIObjectExists(homepageWordmark) } fun verifyHomeComponent() = assertHomeComponent() fun verifyTabCounter(numberOfOpenTabs: String) = - assertItemWithResIdAndTextExists(tabCounter(numberOfOpenTabs)) - - fun verifyWallpaperImageApplied(isEnabled: Boolean) { - if (isEnabled) { - assertTrue( - mDevice.findObject( - UiSelector().resourceId("$packageName:id/wallpaperImageView"), - ).waitForExists(waitingTimeShort), - ) - } else { - assertFalse( - mDevice.findObject( - UiSelector().resourceId("$packageName:id/wallpaperImageView"), - ).waitForExists(waitingTimeShort), - ) - } + assertUIObjectExists(tabCounter(numberOfOpenTabs)) - mDevice.findObject(UiSelector()) - } + fun verifyWallpaperImageApplied(isEnabled: Boolean) = + assertUIObjectExists(itemWithResId("$packageName:id/wallpaperImageView"), exists = isEnabled) // Upgrading users onboarding dialog fun verifyUpgradingUserOnboardingFirstScreen(testRule: ComposeTestRule) { @@ -147,6 +133,58 @@ class HomeScreenRobot { } } + fun verifyFirstOnboardingCard(composeTestRule: ComposeTestRule) { + composeTestRule.also { + it.onNodeWithText( + getStringResource(R.string.juno_onboarding_default_browser_title_nimbus_2), + ).assertExists() + + it.onNodeWithText( + getStringResource(R.string.juno_onboarding_default_browser_description_nimbus_2), + ).assertExists() + + it.onNodeWithText( + getStringResource(R.string.juno_onboarding_default_browser_positive_button), + ).assertExists() + + it.onNodeWithText( + getStringResource(R.string.juno_onboarding_default_browser_negative_button), + ).assertExists() + } + } + + fun verifySecondOnboardingCard(composeTestRule: ComposeTestRule) { + composeTestRule.also { + it.onNodeWithText( + getStringResource(R.string.juno_onboarding_sign_in_title_2), + ).assertExists() + + it.onNodeWithText( + getStringResource(R.string.juno_onboarding_sign_in_description_2), + ).assertExists() + + it.onNodeWithText( + getStringResource(R.string.juno_onboarding_sign_in_positive_button), + ).assertExists() + + it.onNodeWithText( + getStringResource(R.string.juno_onboarding_sign_in_negative_button), + ).assertExists() + } + } + + fun clickNotNowOnboardingButton(composeTestRule: ComposeTestRule) = + composeTestRule.onNodeWithText( + getStringResource(R.string.juno_onboarding_default_browser_negative_button), + ).performClick() + + fun swipeSecondOnboardingCardToRight() = + mDevice.findObject( + UiSelector().textContains( + getStringResource(R.string.juno_onboarding_sign_in_title_2), + ), + ).swipeRight(3) + fun clickGetStartedButton(testRule: ComposeTestRule) = testRule.onNodeWithText(getStringResource(R.string.onboarding_home_get_started_button)).performClick() @@ -175,20 +213,18 @@ class HomeScreenRobot { .performClick() fun verifyCommonMythsLink() = - assertItemContainingTextExists(itemContainingText(getStringResource(R.string.private_browsing_common_myths))) + assertUIObjectExists(itemContainingText(getStringResource(R.string.private_browsing_common_myths))) fun verifyExistingTopSitesList() = assertExistingTopSitesList() fun verifyNotExistingTopSitesList(title: String) = assertNotExistingTopSitesList(title) fun verifySponsoredShortcutDoesNotExist(sponsoredShortcutTitle: String, position: Int) = - assertFalse( - mDevice.findObject( - UiSelector() - .resourceId("$packageName:id/top_site_item") - .index(position - 1), - ).getChild( - UiSelector() - .textContains(sponsoredShortcutTitle), - ).waitForExists(waitingTimeShort), + assertUIObjectExists( + itemWithResIdAndIndex("$packageName:id/top_site_item", index = position - 1) + .getChild( + UiSelector() + .textContains(sponsoredShortcutTitle), + ), + exists = false, ) fun verifyNotExistingSponsoredTopSitesList() = assertSponsoredTopSitesNotDisplayed() fun verifyExistingTopSitesTabs(title: String) { @@ -204,35 +240,31 @@ class HomeScreenRobot { fun verifyJumpBackInSectionIsDisplayed() { scrollToElementByText(getStringResource(R.string.recent_tabs_header)) - assertTrue(jumpBackInSection().waitForExists(waitingTime)) + assertUIObjectExists(itemContainingText(getStringResource(R.string.recent_tabs_header))) } - fun verifyJumpBackInSectionIsNotDisplayed() = assertJumpBackInSectionIsNotDisplayed() + fun verifyJumpBackInSectionIsNotDisplayed() = + assertUIObjectExists(itemContainingText(getStringResource(R.string.recent_tabs_header)), exists = false) fun verifyJumpBackInItemTitle(testRule: ComposeTestRule, itemTitle: String) = assertJumpBackInItemTitle(testRule, itemTitle) fun verifyJumpBackInItemWithUrl(testRule: ComposeTestRule, itemUrl: String) = assertJumpBackInItemWithUrl(testRule, itemUrl) fun verifyJumpBackInShowAllButton() = assertJumpBackInShowAllButton() - fun verifyRecentlyVisitedSectionIsDisplayed() = assertRecentlyVisitedSectionIsDisplayed() - fun verifyRecentlyVisitedSectionIsNotDisplayed() = assertRecentlyVisitedSectionIsNotDisplayed() - fun verifyRecentBookmarksSectionIsDisplayed() = assertRecentBookmarksSectionIsDisplayed() - fun verifyRecentBookmarksSectionIsNotDisplayed() = assertRecentBookmarksSectionIsNotDisplayed() - fun verifyPocketSectionIsDisplayed() = assertPocketSectionIsDisplayed() - fun verifyPocketSectionIsNotDisplayed() = assertPocketSectionIsNotDisplayed() + fun verifyRecentlyVisitedSectionIsDisplayed(exists: Boolean) = assertRecentlyVisitedSectionIsDisplayed(exists) + fun verifyRecentBookmarksSectionIsDisplayed(exists: Boolean) = assertRecentBookmarksSectionIsDisplayed(exists) + fun verifyPocketSectionIsDisplayed(exists: Boolean) = assertPocketSectionIsDisplayed(exists) fun verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed: Boolean, searchTerm: String, groupSize: Int) { // checks if the search group exists in the Recently visited section if (shouldBeDisplayed) { scrollToElementByText("Recently visited") - assertTrue( - mDevice.findObject(UiSelector().text(searchTerm)) - .getFromParent(UiSelector().text("$groupSize sites")) - .waitForExists(waitingTimeShort), + assertUIObjectExists( + itemContainingText(searchTerm) + .getFromParent(UiSelector().text("$groupSize sites")), ) } else { - assertTrue( - mDevice.findObject(UiSelector().text(searchTerm)) - .getFromParent(UiSelector().text("$groupSize sites")) - .waitUntilGone(waitingTimeShort), + assertUIObjectIsGone( + itemContainingText(searchTerm) + .getFromParent(UiSelector().text("$groupSize sites")), ) } } @@ -240,9 +272,9 @@ class HomeScreenRobot { // Collections elements fun verifyCollectionIsDisplayed(title: String, collectionExists: Boolean = true) { if (collectionExists) { - assertTrue(mDevice.findObject(UiSelector().text(title)).waitForExists(waitingTime)) + assertUIObjectExists(itemContainingText(title)) } else { - assertTrue(mDevice.findObject(UiSelector().text(title)).waitUntilGone(waitingTime)) + assertUIObjectIsGone(itemWithText(title)) } } @@ -265,24 +297,10 @@ class HomeScreenRobot { fun verifyThoughtProvokingStories(enabled: Boolean) { if (enabled) { scrollToElementByText(getStringResource(R.string.pocket_stories_header_1)) - assertTrue( - mDevice.findObject( - UiSelector() - .textContains( - getStringResource(R.string.pocket_stories_header_1), - ), - ).waitForExists(waitingTime), - ) + assertUIObjectExists(itemContainingText(getStringResource(R.string.pocket_stories_header_1))) } else { homeScreenList().scrollToEnd(LISTS_MAXSWIPES) - assertFalse( - mDevice.findObject( - UiSelector() - .textContains( - getStringResource(R.string.pocket_stories_header_1), - ), - ).waitForExists(waitingTimeShort), - ) + assertUIObjectExists(itemContainingText(getStringResource(R.string.pocket_stories_header_1)), exists = false) } } @@ -296,12 +314,7 @@ class HomeScreenRobot { for (position in 0..8) { pocketStoriesList .scrollIntoView(UiSelector().index(position)) - - assertTrue( - "Pocket story item at position $position not found.", - mDevice.findObject(UiSelector().index(position)) - .waitForExists(waitingTimeShort), - ) + assertUIObjectExists(itemWithIndex(position)) } } @@ -323,33 +336,16 @@ class HomeScreenRobot { fun verifyDiscoverMoreStoriesButton() { pocketStoriesList .scrollIntoView(UiSelector().text("Discover more")) - assertTrue( - mDevice.findObject(UiSelector().text("Discover more")) - .waitForExists(waitingTimeShort), - ) + assertUIObjectExists(itemWithText("Discover more")) } fun verifyStoriesByTopic(enabled: Boolean) { if (enabled) { scrollToElementByText(getStringResource(R.string.pocket_stories_categories_header)) - assertTrue( - mDevice.findObject( - UiSelector() - .textContains( - getStringResource(R.string.pocket_stories_categories_header), - ), - ).waitForExists(waitingTime), - ) + assertUIObjectExists(itemContainingText(getStringResource(R.string.pocket_stories_categories_header))) } else { homeScreenList().scrollToEnd(LISTS_MAXSWIPES) - assertFalse( - mDevice.findObject( - UiSelector() - .textContains( - getStringResource(R.string.pocket_stories_categories_header), - ), - ).waitForExists(waitingTimeShort), - ) + assertUIObjectExists(itemContainingText(getStringResource(R.string.pocket_stories_categories_header)), exists = false) } } @@ -375,26 +371,16 @@ class HomeScreenRobot { fun verifyPoweredByPocket() { homeScreenList().scrollIntoView(mDevice.findObject(UiSelector().resourceId("pocket.header"))) - assertTrue(mDevice.findObject(UiSelector().resourceId("pocket.header.title")).exists()) + assertUIObjectExists(itemWithResId("pocket.header.title")) } fun verifyCustomizeHomepageButton(enabled: Boolean) { if (enabled) { scrollToElementByText(getStringResource(R.string.browser_menu_customize_home_1)) - assertTrue( - mDevice.findObject( - UiSelector() - .textContains("Customize homepage"), - ).waitForExists(waitingTime), - ) + assertUIObjectExists(itemContainingText("Customize homepage")) } else { homeScreenList().scrollToEnd(LISTS_MAXSWIPES) - assertFalse( - mDevice.findObject( - UiSelector() - .textContains("Customize homepage"), - ).waitForExists(waitingTimeShort), - ) + assertUIObjectExists(itemContainingText("Customize homepage"), exists = false) } } @@ -444,6 +430,10 @@ class HomeScreenRobot { ) } + fun verifyIfInPrivateOrNormalMode(privateBrowsingEnabled: Boolean) { + assert(isPrivateModeEnabled() == privateBrowsingEnabled) + } + class Transition { fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition { @@ -470,14 +460,20 @@ class HomeScreenRobot { fun openThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition { // Issue: https://github.com/mozilla-mobile/fenix/issues/21578 try { + Log.i(TAG, "openThreeDotMenu: Try block") + Log.i(TAG, "openThreeDotMenu: Looking for main menu button") mDevice.waitNotNull( Until.findObject(By.res("$packageName:id/menuButton")), waitingTime, ) } catch (e: AssertionError) { + Log.i(TAG, "openThreeDotMenu: Catch block") mDevice.pressBack() + Log.i(TAG, "openThreeDotMenu: Pressed device back button") } finally { + Log.i(TAG, "openThreeDotMenu: Finally block") threeDotButton().perform(click()) + Log.i(TAG, "openThreeDotMenu: Clicked main menu button") } ThreeDotMenuMainRobot().interact() @@ -503,17 +499,16 @@ class HomeScreenRobot { return SyncSignInRobot.Transition() } - fun togglePrivateBrowsingMode() { - if ( - !itemWithResIdAndDescription( - "$packageName:id/privateBrowsingButton", - "Disable private browsing", - ).exists() - ) { - mDevice.findObject(UiSelector().resourceId("$packageName:id/privateBrowsingButton")) - .waitForExists( - waitingTime, - ) + fun togglePrivateBrowsingMode(switchPBModeOn: Boolean = true) { + // Switch to private browsing homescreen + if (switchPBModeOn && !isPrivateModeEnabled()) { + privateBrowsingButton.waitForExists(waitingTime) + privateBrowsingButton.click() + } + + // Switch to normal browsing homescreen + if (!switchPBModeOn && isPrivateModeEnabled()) { + privateBrowsingButton.waitForExists(waitingTime) privateBrowsingButton.click() } } @@ -672,9 +667,9 @@ class HomeScreenRobot { } fun expandCollection(title: String, interact: CollectionRobot.() -> Unit): CollectionRobot.Transition { - assertItemContainingTextExists(itemContainingText(title)) + assertUIObjectExists(itemContainingText(title)) itemContainingText(title).clickAndWaitForNewWindow(waitingTimeShort) - assertItemWithDescriptionExists(itemWithDescription(getStringResource(R.string.remove_tab_from_collection))) + assertUIObjectExists(itemWithDescription(getStringResource(R.string.remove_tab_from_collection))) CollectionRobot().interact() return CollectionRobot.Transition() @@ -772,6 +767,30 @@ class HomeScreenRobot { BrowserRobot().interact() return BrowserRobot.Transition() } + + fun clickSetAsDefaultBrowserOnboardingButton( + composeTestRule: ComposeTestRule, + interact: SettingsRobot.() -> Unit, + ): SettingsRobot.Transition { + composeTestRule.onNodeWithText( + getStringResource(R.string.juno_onboarding_default_browser_positive_button), + ).performClick() + + SettingsRobot().interact() + return SettingsRobot.Transition() + } + + fun clickSignInOnboardingButton( + composeTestRule: ComposeTestRule, + interact: SyncSignInRobot.() -> Unit, + ): SyncSignInRobot.Transition { + composeTestRule.onNodeWithText( + getStringResource(R.string.juno_onboarding_sign_in_positive_button), + ).performClick() + + SyncSignInRobot().interact() + return SyncSignInRobot.Transition() + } } } @@ -825,7 +844,7 @@ private fun assertHomeComponent() = private fun threeDotButton() = onView(allOf(withId(R.id.menuButton))) private fun assertExistingTopSitesList() = - assertItemWithResIdExists(itemWithResId("$packageName:id/top_sites_list")) + assertUIObjectExists(itemWithResId("$packageName:id/top_sites_list")) private fun assertExistingTopSitesTabs(title: String) { mDevice.findObject( @@ -840,62 +859,51 @@ private fun assertExistingTopSitesTabs(title: String) { } private fun assertSponsoredShortcutLogoIsDisplayed(position: Int) = - assertTrue( - mDevice.findObject( - UiSelector() - .resourceId("$packageName:id/top_site_item") - .index(position - 1), - ).getChild( - UiSelector() - .resourceId("$packageName:id/favicon_card"), - ).waitForExists(waitingTime), + assertUIObjectExists( + itemWithResIdAndIndex(resourceId = "$packageName:id/top_site_item", index = position - 1) + .getChild( + UiSelector() + .resourceId("$packageName:id/favicon_card"), + ), ) private fun assertSponsoredSubtitleIsDisplayed(position: Int) = - assertTrue( - mDevice.findObject( - UiSelector() - .resourceId("$packageName:id/top_site_item") - .index(position - 1), - ).getChild( - UiSelector() - .resourceId("$packageName:id/top_site_subtitle"), - ).waitForExists(waitingTime), + assertUIObjectExists( + itemWithResIdAndIndex(resourceId = "$packageName:id/top_site_item", index = position - 1) + .getChild( + UiSelector() + .resourceId("$packageName:id/top_site_subtitle"), + ), ) private fun assertSponsoredShortcutTitle(sponsoredShortcutTitle: String, position: Int) = - assertTrue( - mDevice.findObject( - UiSelector() - .resourceId("$packageName:id/top_site_item") - .index(position - 1), - ).getChild( - UiSelector() - .textContains(sponsoredShortcutTitle), - ).waitForExists(waitingTime), + assertUIObjectExists( + itemWithResIdAndIndex(resourceId = "$packageName:id/top_site_item", index = position - 1) + .getChild( + UiSelector() + .textContains(sponsoredShortcutTitle), + ), ) private fun assertNotExistingTopSitesList(title: String) { mDevice.findObject(UiSelector().text(title)).waitUntilGone(waitingTime) - - assertFalse( - mDevice.findObject( - UiSelector() - .resourceId("$packageName:id/top_site_title") - .textContains(title), - ).waitForExists(waitingTimeShort), + assertUIObjectExists( + itemWithResIdContainingText( + "$packageName:id/top_site_title", + title, + ), + exists = false, ) } -private fun assertSponsoredTopSitesNotDisplayed() { - assertFalse( - mDevice.findObject( - UiSelector() - .resourceId("$packageName:id/top_site_subtitle") - .textContains(getStringResource(R.string.top_sites_sponsored_label)), - ).waitForExists(waitingTimeShort), +private fun assertSponsoredTopSitesNotDisplayed() = + assertUIObjectExists( + itemWithResIdContainingText( + "$packageName:id/top_site_subtitle", + getStringResource(R.string.top_sites_sponsored_label), + ), + exists = false, ) -} private fun assertTopSiteContextMenuItems() { mDevice.waitNotNull( @@ -908,8 +916,6 @@ private fun assertTopSiteContextMenuItems() { ) } -private fun assertJumpBackInSectionIsNotDisplayed() = assertFalse(jumpBackInSection().waitForExists(waitingTimeShort)) - private fun assertJumpBackInItemTitle(testRule: ComposeTestRule, itemTitle: String) = testRule.onNodeWithTag("recent.tab.title", useUnmergedTree = true).assert(hasText(itemTitle)) @@ -917,44 +923,21 @@ private fun assertJumpBackInItemWithUrl(testRule: ComposeTestRule, itemUrl: Stri testRule.onNodeWithTag("recent.tab.url", useUnmergedTree = true).assert(hasText(itemUrl)) private fun assertJumpBackInShowAllButton() = - assertTrue( - mDevice - .findObject( - UiSelector() - .textContains(getStringResource(R.string.recent_tabs_show_all)), - ).waitForExists(waitingTime), - ) - -private fun assertRecentlyVisitedSectionIsDisplayed() = assertTrue(recentlyVisitedSection().waitForExists(waitingTime)) - -private fun assertRecentlyVisitedSectionIsNotDisplayed() = assertFalse(recentlyVisitedSection().waitForExists(waitingTimeShort)) + assertUIObjectExists(itemContainingText(getStringResource(R.string.recent_tabs_show_all))) -private fun assertRecentBookmarksSectionIsDisplayed() = - assertTrue(recentBookmarksSection().waitForExists(waitingTime)) +private fun assertRecentlyVisitedSectionIsDisplayed(exists: Boolean) = + assertUIObjectExists(itemContainingText(getStringResource(R.string.history_metadata_header_2)), exists = exists) -private fun assertRecentBookmarksSectionIsNotDisplayed() = - assertFalse(recentBookmarksSection().waitForExists(waitingTimeShort)) +private fun assertRecentBookmarksSectionIsDisplayed(exists: Boolean) = + assertUIObjectExists(itemContainingText(getStringResource(R.string.recently_saved_title)), exists = exists) -private fun assertPocketSectionIsDisplayed() = assertTrue(pocketSection().waitForExists(waitingTime)) - -private fun assertPocketSectionIsNotDisplayed() = assertFalse(pocketSection().waitForExists(waitingTimeShort)) +private fun assertPocketSectionIsDisplayed(exists: Boolean) = + assertUIObjectExists(itemContainingText(getStringResource(R.string.pocket_stories_header_1)), exists = exists) private fun saveTabsToCollectionButton() = onView(withId(R.id.add_tabs_to_collections_button)) private fun tabsCounter() = onView(withId(R.id.tab_button)) -private fun jumpBackInSection() = - mDevice.findObject(UiSelector().textContains(getStringResource(R.string.recent_tabs_header))) - -private fun recentlyVisitedSection() = - mDevice.findObject(UiSelector().textContains(getStringResource(R.string.history_metadata_header_2))) - -private fun recentBookmarksSection() = - mDevice.findObject(UiSelector().textContains(getStringResource(R.string.recently_saved_title))) - -private fun pocketSection() = - mDevice.findObject(UiSelector().textContains(getStringResource(R.string.pocket_stories_header_1))) - private fun sponsoredShortcut(sponsoredShortcutTitle: String) = mDevice.findObject( By @@ -969,6 +952,13 @@ private val homeScreen = itemWithResId("$packageName:id/homeLayout") private val privateBrowsingButton = itemWithResId("$packageName:id/privateBrowsingButton") + +private fun isPrivateModeEnabled(): Boolean = + itemWithResIdAndDescription( + "$packageName:id/privateBrowsingButton", + "Disable private browsing", + ).exists() + private val homepageWordmark = itemWithResId("$packageName:id/wordmark") diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NavigationToolbarRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NavigationToolbarRobot.kt index 629f994b24..0aafaef643 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NavigationToolbarRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NavigationToolbarRobot.kt @@ -8,6 +8,7 @@ package org.mozilla.fenix.ui.robots import android.net.Uri import android.os.Build +import android.util.Log import androidx.compose.ui.test.onNodeWithTag import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.Espresso.onView @@ -30,18 +31,22 @@ import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.Until import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.not -import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.mozilla.fenix.R import org.mozilla.fenix.helpers.Constants import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION +import org.mozilla.fenix.helpers.Constants.TAG +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.HomeActivityComposeTestRule +import org.mozilla.fenix.helpers.MatcherHelper.assertItemTextEquals +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId +import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText +import org.mozilla.fenix.helpers.MatcherHelper.itemWithText import org.mozilla.fenix.helpers.SessionLoadedIdlingResource import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.TestHelper.waitForObjects @@ -76,32 +81,15 @@ class NavigationToolbarRobot { readerViewToggle().click() } - fun verifyClipboardSuggestionsAreDisplayed(link: String = "", shouldBeDisplayed: Boolean) { - when (shouldBeDisplayed) { - true -> { - assertTrue( - mDevice.findObject(UiSelector().resourceId("$packageName:id/fill_link_from_clipboard")) - .waitForExists(waitingTime), - ) - - assertTrue( - mDevice.findObject(UiSelector().resourceId("$packageName:id/clipboard_url").text(link)) - .waitForExists(waitingTime), - ) - } - false -> { - assertFalse( - mDevice.findObject(UiSelector().resourceId("$packageName:id/fill_link_from_clipboard")) - .waitForExists(waitingTimeShort), - ) - - assertFalse( - mDevice.findObject(UiSelector().resourceId("$packageName:id/clipboard_url").text(link)) - .waitForExists(waitingTimeShort), - ) - } - } - } + fun verifyClipboardSuggestionsAreDisplayed(link: String = "", shouldBeDisplayed: Boolean) = + assertUIObjectExists( + itemWithResId("$packageName:id/fill_link_from_clipboard"), + itemWithResIdAndText( + "$packageName:id/clipboard_url", + link, + ), + exists = shouldBeDisplayed, + ) fun longClickEditModeToolbar() = mDevice.findObject(By.res("$packageName:id/mozac_browser_toolbar_edit_url_view")).click(LONG_CLICK_DURATION) @@ -125,17 +113,13 @@ class NavigationToolbarRobot { // New unified search UI selector fun verifySearchBarPlaceholder(text: String) { urlBar().waitForExists(waitingTime) - assertTrue( - urlBar().text == text, - ) + assertItemTextEquals(urlBar(), expectedText = text) } // New unified search UI selector fun verifyDefaultSearchEngine(engineName: String) = - assertTrue( - searchSelectorButton - .getChild(UiSelector().description(engineName)) - .waitForExists(waitingTime), + assertUIObjectExists( + searchSelectorButton.getChild(UiSelector().description(engineName)), ) fun verifyTextSelectionOptions(vararg textSelectionOptions: String) { @@ -154,19 +138,18 @@ class NavigationToolbarRobot { sessionLoadedIdlingResource = SessionLoadedIdlingResource() openEditURLView() + Log.i(TAG, "enterURLAndEnterToBrowser: Opened edit mode URL view") awesomeBar().setText(url.toString()) + Log.i(TAG, "enterURLAndEnterToBrowser: Set toolbar text to: $url") mDevice.pressEnter() + Log.i(TAG, "enterURLAndEnterToBrowser: Clicked enter on keyboard, submitted query") runWithIdleRes(sessionLoadedIdlingResource) { assertTrue( - mDevice.findObject( - UiSelector().resourceId("$packageName:id/browserLayout"), - ).waitForExists(waitingTime) || mDevice.findObject( - UiSelector().resourceId("$packageName:id/download_button"), - ).waitForExists(waitingTime) || mDevice.findObject( - UiSelector().text(getStringResource(R.string.tcp_cfr_message)), - ).waitForExists(waitingTime), + itemWithResId("$packageName:id/browserLayout").waitForExists(waitingTime) || + itemWithResId("$packageName:id/download_button").waitForExists(waitingTime) || + itemWithText(getStringResource(R.string.tcp_cfr_message)).waitForExists(waitingTime), ) } @@ -290,8 +273,9 @@ class NavigationToolbarRobot { } fun openTabButtonShortcutsMenu(interact: NavigationToolbarRobot.() -> Unit): Transition { - mDevice.waitNotNull(Until.findObject(By.desc("Tabs"))) + mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/counter_root"))) tabsCounter().click(LONG_CLICK_DURATION) + Log.i(TAG, "Tabs counter long-click successful.") NavigationToolbarRobot().interact() return Transition() @@ -309,6 +293,7 @@ class NavigationToolbarRobot { ViewActions.click(), ), ) + Log.i(TAG, "Clicked the tab shortcut Close tab button.") NavigationToolbarRobot().interact() return Transition() @@ -316,7 +301,7 @@ class NavigationToolbarRobot { fun openNewTabFromShortcutsMenu(interact: SearchRobot.() -> Unit): SearchRobot.Transition { mDevice.waitForIdle(waitingTime) - + Log.i(TAG, "Looking for tab shortcut New tab button.") onView(withId(R.id.mozac_browser_menu_recyclerView)) .perform( RecyclerViewActions.actionOnItem( @@ -326,6 +311,7 @@ class NavigationToolbarRobot { ViewActions.click(), ), ) + Log.i(TAG, "Clicked the tab shortcut New tab button.") SearchRobot().interact() return SearchRobot.Transition() @@ -333,7 +319,7 @@ class NavigationToolbarRobot { fun openNewPrivateTabFromShortcutsMenu(interact: SearchRobot.() -> Unit): SearchRobot.Transition { mDevice.waitForIdle(waitingTime) - + Log.i(TAG, "Looking for tab shortcut New private tab button.") onView(withId(R.id.mozac_browser_menu_recyclerView)) .perform( RecyclerViewActions.actionOnItem( @@ -343,6 +329,7 @@ class NavigationToolbarRobot { ViewActions.click(), ), ) + Log.i(TAG, "Clicked the tab shortcut New private tab button.") SearchRobot().interact() return SearchRobot.Transition() @@ -375,15 +362,11 @@ fun navigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationTo } fun openEditURLView() { - mDevice.waitNotNull( - Until.findObject(By.res("$packageName:id/toolbar")), - waitingTime, - ) + urlBar().waitForExists(waitingTime) urlBar().click() - mDevice.waitNotNull( - Until.findObject(By.res("$packageName:id/mozac_browser_toolbar_edit_url_view")), - waitingTime, - ) + Log.i(TAG, "openEditURLView: URL bar clicked.") + itemWithResId("$packageName:id/mozac_browser_toolbar_edit_url_view").waitForExists(waitingTime) + Log.i(TAG, "openEditURLView: Edit URL bar displayed.") } private fun assertNoHistoryBookmarks() { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NotificationRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NotificationRobot.kt index 1ef091ed17..7398746e95 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NotificationRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NotificationRobot.kt @@ -6,13 +6,14 @@ package org.mozilla.fenix.ui.robots import android.app.NotificationManager import android.content.Context +import android.util.Log import androidx.test.uiautomator.UiScrollable import androidx.test.uiautomator.UiSelector -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue import org.mozilla.fenix.helpers.Constants.RETRY_COUNT -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndTextExists +import org.mozilla.fenix.helpers.Constants.TAG +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText +import org.mozilla.fenix.helpers.MatcherHelper.itemWithText import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort import org.mozilla.fenix.helpers.TestHelper @@ -29,34 +30,43 @@ class NotificationRobot { while (!notificationFound) { scrollToEnd() + Log.i(TAG, "verifySystemNotificationExists: Scrolling to the end of the notification tray") + Log.i(TAG, "verifySystemNotificationExists: Looking for $notificationMessage notification") notificationFound = mDevice.findObject(notification).waitForExists(waitingTime) } - assertTrue(notificationFound) + assertUIObjectExists(itemWithText(notificationMessage)) } fun clearNotifications() { if (clearButton.exists()) { + Log.i(TAG, "clearNotifications: Verified that clear notifications button exists") clearButton.click() + Log.i(TAG, "clearNotifications: Clicked clear notifications button") } else { scrollToEnd() + Log.i(TAG, "clearNotifications: Scrolled to end of notifications tray") if (clearButton.exists()) { + Log.i(TAG, "clearNotifications: Verified that clear notifications button exists") clearButton.click() + Log.i(TAG, "clearNotifications: Clicked clear notifications button") } else if (notificationTray().exists()) { mDevice.pressBack() + Log.i(TAG, "clearNotifications: Dismiss notifications tray by clicking device back button") } } } fun cancelAllShownNotifications() { cancelAll() + Log.i(TAG, "cancelAllShownNotifications: Canceled all system notifications") } fun verifySystemNotificationDoesNotExist(notificationMessage: String) { + Log.i(TAG, "verifySystemNotificationDoesNotExist: Waiting for $notificationMessage notification to be gone") mDevice.findObject(UiSelector().textContains(notificationMessage)).waitUntilGone(waitingTime) - assertFalse( - mDevice.findObject(UiSelector().textContains(notificationMessage)).waitForExists(waitingTimeShort), - ) + assertUIObjectExists(itemContainingText(notificationMessage), exists = false) + Log.i(TAG, "verifySystemNotificationDoesNotExist: Verified that $notificationMessage notification does not exist") } fun verifyPrivateTabsNotification() { @@ -71,42 +81,49 @@ class NotificationRobot { fun clickDownloadNotificationControlButton(action: String) { for (i in 1..RETRY_COUNT) { + Log.i(TAG, "clickPageObject: For loop i = $i") try { - assertItemWithResIdAndTextExists(downloadSystemNotificationButton(action)) + assertUIObjectExists(downloadSystemNotificationButton(action)) downloadSystemNotificationButton(action).clickAndWaitForNewWindow(waitingTimeShort) - assertItemWithResIdAndTextExists( + Log.i(TAG, "clickDownloadNotificationControlButton: Clicked app notification $action button and waits for a new window for $waitingTimeShort ms") + assertUIObjectExists( downloadSystemNotificationButton(action), exists = false, ) break } catch (e: AssertionError) { + Log.i(TAG, "clickDownloadNotificationControlButton: Catch block") if (i == RETRY_COUNT) { throw e } mDevice.waitForWindowUpdate(packageName, waitingTimeShort) + Log.i(TAG, "clickDownloadNotificationControlButton: Waited $waitingTimeShort ms for window update") } } } - fun verifyMediaSystemNotificationButtonState(action: String) { - assertTrue(mediaSystemNotificationButton(action).waitForExists(waitingTime)) - } + fun verifyMediaSystemNotificationButtonState(action: String) = + assertUIObjectExists(mediaSystemNotificationButton(action)) fun expandNotificationMessage() { while (!notificationHeader.exists()) { scrollToEnd() + Log.i(TAG, "expandNotificationMessage: Scrolled to end of notification tray") } if (notificationHeader.exists()) { // expand the notification notificationHeader.click() + Log.i(TAG, "expandNotificationMessage: Clicked the app notification") // double check if notification actions are viewable by checking for action existence; otherwise scroll again while (!mDevice.findObject(UiSelector().resourceId("android:id/action0")).exists() && !mDevice.findObject(UiSelector().resourceId("android:id/actions_container")).exists() ) { + Log.i(TAG, "expandNotificationMessage: App notification action buttons do not exist") scrollToEnd() + Log.i(TAG, "expandNotificationMessage: Scrolled to end of notification tray") } } } @@ -119,9 +136,12 @@ class NotificationRobot { ) { // In case it fails, retry max 3x the swipe action on download system notifications for (i in 1..RETRY_COUNT) { + Log.i(TAG, "swipeDownloadNotification: For loop i = $i") try { + Log.i(TAG, "swipeDownloadNotification: Try block") var retries = 0 while (itemContainingText(appName).exists() && retries++ < 3) { + Log.i(TAG, "swipeDownloadNotification: While loop retries = $retries") // Swipe left the download system notification if (direction == "Left") { itemContainingText(appName) @@ -129,6 +149,7 @@ class NotificationRobot { it.waitForExists(waitingTime) it.swipeLeft(3) } + Log.i(TAG, "swipeDownloadNotification: Swiped left download notification") } else { // Swipe right the download system notification itemContainingText(appName) @@ -136,17 +157,20 @@ class NotificationRobot { it.waitForExists(waitingTime) it.swipeRight(3) } + Log.i(TAG, "swipeDownloadNotification: Swiped right download notification") } } // Not all download related system notifications can be dismissed if (shouldDismissNotification) { - assertFalse(itemContainingText(appName).waitForExists(waitingTimeShort)) + assertUIObjectExists(itemContainingText(appName), exists = false) } else { - assertTrue(itemContainingText(appName).waitForExists(waitingTimeShort)) + assertUIObjectExists(itemContainingText(appName)) + Log.i(TAG, "swipeDownloadNotification: Verified that $appName notification exist") } break } catch (e: AssertionError) { + Log.i(TAG, "swipeDownloadNotification: Catch block") if (i == RETRY_COUNT) { throw e } else { @@ -157,6 +181,7 @@ class NotificationRobot { if (canExpandNotification) { // In all cases the download system notification title will be the app name verifySystemNotificationExists(appName) + Log.i(TAG, "swipeDownloadNotification: Verified that $appName notification exist") expandNotificationMessage() } else { // Using the download completed system notification summary to bring in to view an properly verify it @@ -169,17 +194,17 @@ class NotificationRobot { } fun clickNotification(notificationMessage: String) { + Log.i(TAG, "clickNotification: Looking for $notificationMessage notification") mDevice.findObject(UiSelector().text(notificationMessage)).waitForExists(waitingTime) mDevice.findObject(UiSelector().text(notificationMessage)).clickAndWaitForNewWindow(waitingTimeShort) + Log.i(TAG, "clickNotification: Clicked $notificationMessage notification and waiting for $waitingTimeShort ms for a new window") } class Transition { fun clickClosePrivateTabsNotification(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { try { - assertTrue( - closePrivateTabsNotification().exists(), - ) + assertUIObjectExists(closePrivateTabsNotification()) } catch (e: AssertionError) { notificationTray().flingToEnd(1) } @@ -192,6 +217,7 @@ class NotificationRobot { fun closeNotificationTray(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { mDevice.pressBack() + Log.i(TAG, "closeNotificationTray: Closed notification tray using device back button") BrowserRobot().interact() return BrowserRobot.Transition() diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/PwaRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/PwaRobot.kt index b4f88852df..f9e050526c 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/PwaRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/PwaRobot.kt @@ -5,14 +5,14 @@ package org.mozilla.fenix.ui.robots import androidx.test.uiautomator.UiSelector -import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue -import org.mozilla.fenix.helpers.TestHelper.isExternalAppBrowserActivityInCurrentTask +import org.mozilla.fenix.helpers.AppAndSystemHelper.isExternalAppBrowserActivityInCurrentTask +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName class PwaRobot { - fun verifyCustomTabToolbarIsNotDisplayed() = assertFalse(customTabToolbar().exists()) + fun verifyCustomTabToolbarIsNotDisplayed() = assertUIObjectExists(customTabToolbar(), exists = false) fun verifyPwaActivityInCurrentTask() = assertTrue(isExternalAppBrowserActivityInCurrentTask()) class Transition diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/RecentlyClosedTabsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/RecentlyClosedTabsRobot.kt index a2cd5d2342..d77962cfb1 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/RecentlyClosedTabsRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/RecentlyClosedTabsRobot.kt @@ -18,7 +18,7 @@ import org.hamcrest.Matchers import org.hamcrest.Matchers.allOf import org.mozilla.fenix.R import org.mozilla.fenix.helpers.HomeActivityComposeTestRule -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndTextExists +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort @@ -57,7 +57,7 @@ class RecentlyClosedTabsRobot { } fun verifyRecentlyClosedTabsPageTitle(title: String) = - assertItemWithResIdAndTextExists( + assertUIObjectExists( recentlyClosedTabsPageTitle(title), ) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt index ecffa0e59b..d2ca0ce4c8 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt @@ -29,22 +29,25 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.uiautomator.By import androidx.test.uiautomator.UiSelector import org.hamcrest.CoreMatchers.allOf -import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.AppAndSystemHelper.grantSystemPermission +import org.mozilla.fenix.helpers.AppAndSystemHelper.isPackageInstalled import org.mozilla.fenix.helpers.Constants import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION import org.mozilla.fenix.helpers.Constants.RETRY_COUNT import org.mozilla.fenix.helpers.Constants.SPEECH_RECOGNITION -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource +import org.mozilla.fenix.helpers.MatcherHelper.assertItemTextContains +import org.mozilla.fenix.helpers.MatcherHelper.assertItemTextEquals +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectIsGone import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId +import org.mozilla.fenix.helpers.MatcherHelper.itemWithText import org.mozilla.fenix.helpers.SessionLoadedIdlingResource import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort -import org.mozilla.fenix.helpers.TestHelper.getStringResource -import org.mozilla.fenix.helpers.TestHelper.grantSystemPermission -import org.mozilla.fenix.helpers.TestHelper.isPackageInstalled import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.TestHelper.waitForObjects @@ -53,38 +56,19 @@ import org.mozilla.fenix.helpers.TestHelper.waitForObjects * Implementation of Robot Pattern for the search fragment. */ class SearchRobot { - fun verifySearchView() = - assertTrue( - mDevice.findObject( - UiSelector().resourceId("$packageName:id/search_wrapper"), - ).waitForExists(waitingTime), - ) + fun verifySearchView() = assertUIObjectExists(itemWithResId("$packageName:id/search_wrapper")) fun verifySearchToolbar(isDisplayed: Boolean) = - assertItemWithResIdExists( + assertUIObjectExists( itemWithResId("$packageName:id/mozac_browser_toolbar_edit_url_view"), exists = isDisplayed, ) - fun verifyScanButtonVisibility(visible: Boolean = true) { - if (visible) { - assertTrue( - scanButton.waitForExists(waitingTime), - ) - } else { - assertTrue( - scanButton.waitUntilGone(waitingTime), - ) - } - } + fun verifyScanButtonVisibility(visible: Boolean = true) = + assertUIObjectExists(scanButton, exists = visible) - fun verifyVoiceSearchButtonVisibility(enabled: Boolean) { - if (enabled) { - assertTrue(voiceSearchButton.waitForExists(waitingTime)) - } else { - assertFalse(voiceSearchButton.waitForExists(waitingTimeShort)) - } - } + fun verifyVoiceSearchButtonVisibility(enabled: Boolean) = + assertUIObjectExists(voiceSearchButton, exists = enabled) // Device or AVD requires a Google Services Android OS installation fun startVoiceSearch() { @@ -154,33 +138,14 @@ class SearchRobot { } } - fun verifyAllowSuggestionsInPrivateModeDialog() { - assertTrue( - mDevice.findObject( - UiSelector().text(getStringResource(R.string.search_suggestions_onboarding_title)), - ).waitForExists(waitingTime), - ) - assertTrue( - mDevice.findObject( - UiSelector().text(getStringResource(R.string.search_suggestions_onboarding_text)), - ).exists(), - ) - assertTrue( - mDevice.findObject( - UiSelector().text("Learn more"), - ).exists(), + fun verifyAllowSuggestionsInPrivateModeDialog() = + assertUIObjectExists( + itemWithText(getStringResource(R.string.search_suggestions_onboarding_title)), + itemWithText(getStringResource(R.string.search_suggestions_onboarding_text)), + itemWithText("Learn more"), + itemWithText(getStringResource(R.string.search_suggestions_onboarding_allow_button)), + itemWithText(getStringResource(R.string.search_suggestions_onboarding_do_not_allow_button)), ) - assertTrue( - mDevice.findObject( - UiSelector().text(getStringResource(R.string.search_suggestions_onboarding_allow_button)), - ).exists(), - ) - assertTrue( - mDevice.findObject( - UiSelector().text(getStringResource(R.string.search_suggestions_onboarding_do_not_allow_button)), - ).exists(), - ) - } fun denySuggestionsInPrivateMode() { mDevice.findObject( @@ -194,37 +159,28 @@ class SearchRobot { ).click() } - fun verifySearchSelectorButton() { - assertTrue(searchSelectorButton.waitForExists(waitingTime)) - } + fun verifySearchSelectorButton() = assertUIObjectExists(searchSelectorButton) fun clickSearchSelectorButton() { searchSelectorButton.waitForExists(waitingTime) searchSelectorButton.click() } - fun verifySearchEngineIcon(name: String) = - assertTrue(itemWithDescription(name).waitForExists(waitingTime)) + fun verifySearchEngineIcon(name: String) = assertUIObjectExists(itemWithDescription(name)) fun verifySearchBarPlaceholder(text: String) { browserToolbarEditView().waitForExists(waitingTime) - assertTrue( - browserToolbarEditView().text == text, - ) + assertItemTextEquals(browserToolbarEditView(), expectedText = text) } fun verifySearchShortcutListContains(vararg searchEngineName: String, shouldExist: Boolean = true) { searchEngineName.forEach { if (shouldExist) { - assertTrue( - searchShortcutList.getChild(UiSelector().text(it)) - .waitForExists(waitingTimeShort), + assertUIObjectExists( + searchShortcutList.getChild(UiSelector().text(it)), ) } else { - assertTrue( - searchShortcutList.getChild(UiSelector().text(it)) - .waitUntilGone(waitingTimeShort), - ) + assertUIObjectIsGone(searchShortcutList.getChild(UiSelector().text(it))) } } } @@ -304,7 +260,7 @@ class SearchRobot { } fun verifyTranslatedFocusedNavigationToolbar(toolbarHintString: String) = - assertTrue(browserToolbarEditView().text.contains(toolbarHintString)) + assertItemTextContains(browserToolbarEditView(), itemText = toolbarHintString) fun verifyTypedToolbarText(expectedText: String) { mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar")) @@ -323,9 +279,9 @@ class SearchRobot { onView(withId(R.id.toolbar)) .check( if (bottomPosition) { - PositionAssertions.isCompletelyBelow(withId(R.id.pill_wrapper_divider)) + PositionAssertions.isCompletelyBelow(withId(R.id.keyboard_divider)) } else { - PositionAssertions.isCompletelyAbove(withId(R.id.pill_wrapper_divider)) + PositionAssertions.isCompletelyAbove(withId(R.id.keyboard_divider)) }, ) } @@ -334,14 +290,13 @@ class SearchRobot { private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource fun dismissSearchBar(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { - mDevice.waitForIdle() - closeSoftKeyboard() - mDevice.pressBack() try { - assertTrue(searchWrapper().waitUntilGone(waitingTimeShort)) + searchWrapper().waitForExists(waitingTime) + mDevice.pressBack() + assertUIObjectIsGone(searchWrapper()) } catch (e: AssertionError) { mDevice.pressBack() - assertTrue(searchWrapper().waitUntilGone(waitingTimeShort)) + assertUIObjectIsGone(searchWrapper()) } HomeScreenRobot().interact() @@ -364,11 +319,7 @@ class SearchRobot { mDevice.pressEnter() runWithIdleRes(sessionLoadedIdlingResource) { - assertTrue( - mDevice.findObject( - UiSelector().resourceId("$packageName:id/browserLayout"), - ).waitForExists(waitingTime), - ) + assertUIObjectExists(itemWithResId("$packageName:id/browserLayout")) } BrowserRobot().interact() diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt index 3093433030..c459ce42dc 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt @@ -8,6 +8,7 @@ package org.mozilla.fenix.ui.robots import android.content.Intent import android.net.Uri +import android.util.Log import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.Espresso.onView import androidx.test.espresso.ViewInteraction @@ -40,20 +41,20 @@ import junit.framework.AssertionFailedError import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers.endsWith import org.hamcrest.Matchers.allOf -import org.junit.Assert.assertTrue import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.AppAndSystemHelper.isPackageInstalled import org.mozilla.fenix.helpers.Constants.LISTS_MAXSWIPES import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_PLAY_SERVICES import org.mozilla.fenix.helpers.Constants.RETRY_COUNT -import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists +import org.mozilla.fenix.helpers.Constants.TAG +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithText import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort import org.mozilla.fenix.helpers.TestHelper.appName -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.hasCousin -import org.mozilla.fenix.helpers.TestHelper.isPackageInstalled import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText @@ -76,7 +77,7 @@ class SettingsRobot { fun verifyAccessibilityButton() = assertAccessibilityButton() fun verifySetAsDefaultBrowserButton() = assertSetAsDefaultBrowserButton() fun verifyTabsButton() = - assertItemContainingTextExists(itemContainingText(getStringResource(R.string.preferences_tabs))) + assertUIObjectExists(itemContainingText(getStringResource(R.string.preferences_tabs))) fun verifyHomepageButton() = assertHomepageButton() fun verifyAutofillButton() = assertAutofillButton() fun verifyLanguageButton() = assertLanguageButton() @@ -106,9 +107,6 @@ class SettingsRobot { fun verifyPrivacyHeading() = assertPrivacyHeading() fun verifyHTTPSOnlyModeButton() = assertHTTPSOnlyModeButton() - fun verifyCookieBannerReductionButton() = - onView(withText(R.string.preferences_cookie_banner_reduction)).check(matches(isDisplayed())) - fun verifyEnhancedTrackingProtectionButton() = assertEnhancedTrackingProtectionButton() fun verifyLoginsAndPasswordsButton() = assertLoginsAndPasswordsButton() fun verifyPrivateBrowsingButton() = assertPrivateBrowsingButton() @@ -188,8 +186,8 @@ class SettingsRobot { // ABOUT SECTION fun verifyAboutHeading() = assertAboutHeading() - fun verifyRateOnGooglePlay() = assertTrue(rateOnGooglePlayHeading().waitForExists(waitingTime)) - fun verifyAboutFirefoxPreview() = assertTrue(aboutFirefoxHeading().waitForExists(waitingTime)) + fun verifyRateOnGooglePlay() = assertUIObjectExists(rateOnGooglePlayHeading()) + fun verifyAboutFirefoxPreview() = assertUIObjectExists(aboutFirefoxHeading()) fun verifyGooglePlayRedirect() = assertGooglePlayRedirect() fun verifySettingsOptionSummary(setting: String, summary: String) { @@ -210,6 +208,14 @@ class SettingsRobot { return HomeScreenRobot.Transition() } + fun goBackToOnboardingScreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { + mDevice.pressBack() + mDevice.waitForIdle(waitingTimeShort) + + HomeScreenRobot().interact() + return HomeScreenRobot.Transition() + } + fun goBackToBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { goBackButton().click() @@ -264,8 +270,10 @@ class SettingsRobot { fun openAutofillSubMenu(interact: SettingsSubMenuAutofillRobot.() -> Unit): SettingsSubMenuAutofillRobot.Transition { mDevice.findObject(UiSelector().textContains(getStringResource(R.string.preferences_autofill))) .also { + Log.i(TAG, "openAutofillSubMenu: Looking for \"Autofill\" settings button") it.waitForExists(waitingTime) it.click() + Log.i(TAG, "openAutofillSubMenu: Clicked \"Autofill\" settings button") } SettingsSubMenuAutofillRobot().interact() @@ -311,14 +319,6 @@ class SettingsRobot { return SettingsSubMenuSetDefaultBrowserRobot.Transition() } - fun openCookieBannerReductionSubMenu(interact: SettingsSubMenuCookieBannerReductionRobot.() -> Unit): SettingsSubMenuCookieBannerReductionRobot.Transition { - scrollToElementByText(getStringResource(R.string.preferences_cookie_banner_reduction)) - itemContainingText(getStringResource(R.string.preferences_cookie_banner_reduction)).click() - - SettingsSubMenuCookieBannerReductionRobot().interact() - return SettingsSubMenuCookieBannerReductionRobot.Transition() - } - fun openEnhancedTrackingProtectionSubMenu(interact: SettingsSubMenuEnhancedTrackingProtectionRobot.() -> Unit): SettingsSubMenuEnhancedTrackingProtectionRobot.Transition { scrollToElementByText("Enhanced Tracking Protection") fun enhancedTrackingProtectionButton() = @@ -428,8 +428,7 @@ class SettingsRobot { fun openExperimentsMenu(interact: SettingsSubMenuExperimentsRobot.() -> Unit): SettingsSubMenuExperimentsRobot.Transition { scrollToElementByText("Nimbus Experiments") - fun nimbusExperimentsButton() = mDevice.findObject(textContains("Nimbus Experiments")) - nimbusExperimentsButton().click() + onView(withText(getStringResource(R.string.preferences_nimbus_experiments))).click() SettingsSubMenuExperimentsRobot().interact() return SettingsSubMenuExperimentsRobot.Transition() @@ -640,10 +639,7 @@ private fun aboutFirefoxHeading(): UiObject { for (i in 1..RETRY_COUNT) { try { settingsList().scrollToEnd(LISTS_MAXSWIPES) - assertTrue( - mDevice.findObject(UiSelector().text("About $appName")) - .waitForExists(waitingTime), - ) + assertUIObjectExists(itemWithText("About $appName")) break } catch (e: AssertionError) { 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 3b25a269d4..dd0133f392 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 @@ -26,14 +26,13 @@ import androidx.test.uiautomator.UiSelector import mozilla.components.support.utils.ext.getPackageInfoCompat import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.containsString -import org.junit.Assert.assertTrue import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.R import org.mozilla.fenix.helpers.Constants.LISTS_MAXSWIPES -import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.TestHelper import org.mozilla.fenix.helpers.TestHelper.appName -import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.settings.SupportUtils import java.text.SimpleDateFormat @@ -156,11 +155,7 @@ private fun assertCrashes() { .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .perform(click()) - assertTrue( - mDevice.findObject( - UiSelector().textContains("No crash reports have been submitted."), - ).waitForExists(waitingTime), - ) + assertUIObjectExists(itemContainingText("No crash reports have been submitted.")) for (i in 1..3) { Espresso.pressBack() diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.kt index ff72dcae0a..799f3a9913 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.kt @@ -16,7 +16,7 @@ import androidx.test.rule.ActivityTestRule import org.hamcrest.CoreMatchers.allOf import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R -import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources +import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource import org.mozilla.fenix.helpers.click diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerRobot.kt index 9591f596d5..76de054218 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerRobot.kt @@ -25,20 +25,20 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withParent import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiScrollable import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.Until import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.instanceOf -import org.hamcrest.CoreMatchers.not -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue import org.mozilla.fenix.R import org.mozilla.fenix.helpers.Constants.RETRY_COUNT +import org.mozilla.fenix.helpers.Constants.TAG import org.mozilla.fenix.helpers.HomeActivityIntentTestRule +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.itemWithText import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeLong -import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort import org.mozilla.fenix.helpers.TestHelper.appName import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName @@ -52,21 +52,8 @@ import org.mozilla.fenix.helpers.ext.waitNotNull */ class SettingsSubMenuAddonsManagerRobot { - fun verifyAddonsListIsDisplayed(shouldBeDisplayed: Boolean) { - if (shouldBeDisplayed) { - assertTrue( - mDevice.findObject( - UiSelector().resourceId("$packageName:id/add_ons_list"), - ).waitForExists(waitingTime), - ) - } else { - assertTrue( - mDevice.findObject( - UiSelector().resourceId("$packageName:id/add_ons_list"), - ).waitUntilGone(waitingTime), - ) - } - } + fun verifyAddonsListIsDisplayed(shouldBeDisplayed: Boolean) = + assertUIObjectExists(addonsList(), exists = shouldBeDisplayed) fun verifyAddonPermissionPrompt(addonName: String) { mDevice.waitNotNull(Until.findObject(By.text("Add $addonName?")), waitingTime) @@ -84,36 +71,47 @@ class SettingsSubMenuAddonsManagerRobot { } fun clickInstallAddon(addonName: String) { - mDevice.waitNotNull( - Until.findObject(By.textContains(addonName)), - waitingTime, + Log.i(TAG, "clickInstallAddon: Looking for $addonName install button") + addonsList().waitForExists(waitingTime) + addonsList().scrollIntoView( + mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/details_container") + .childSelector(UiSelector().text(addonName)), + ), ) - - installButtonForAddon(addonName) - .check(matches(isCompletelyDisplayed())) - .perform(click()) + addonsList().ensureFullyVisible( + mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/details_container") + .childSelector(UiSelector().text(addonName)), + ), + ) + Log.i(TAG, "clickInstallAddon: Found $addonName install button") + installButtonForAddon(addonName).click() + Log.i(TAG, "clickInstallAddon: Clicked Install $addonName button") } fun verifyAddonInstallCompleted(addonName: String, activityTestRule: HomeActivityIntentTestRule) { for (i in 1..RETRY_COUNT) { try { - assertFalse( - mDevice.findObject(UiSelector().text("Failed to install $addonName")) - .waitForExists(waitingTimeShort), - ) - - assertTrue( - mDevice.findObject(UiSelector().text("Okay, Got it")) - .waitForExists(waitingTimeLong), - ) + assertUIObjectExists(itemWithText("Okay, Got it"), waitingTime = waitingTimeLong) + break } catch (e: AssertionError) { if (i == RETRY_COUNT) { throw e } else { - Log.e("TestLog", "Addon failed to install on try #$i") + Log.i(TAG, "verifyAddonInstallCompleted: $addonName failed to install on try #$i") restartApp(activityTestRule) - installAddon(addonName) + homeScreen { + }.openThreeDotMenu { + }.openAddonsManagerMenu { + scrollToElementByText(addonName) + clickInstallAddon(addonName) + verifyAddonPermissionPrompt(addonName) + acceptPermissionToInstallAddon() + } } } } @@ -152,21 +150,18 @@ class SettingsSubMenuAddonsManagerRobot { fun verifyAddonCanBeInstalled(addonName: String) = assertAddonCanBeInstalled(addonName) fun selectAllowInPrivateBrowsing() { - assertTrue( - "Addon install confirmation prompt not displayed", - mDevice.findObject(UiSelector().text("Allow in private browsing")) - .waitForExists(waitingTimeLong), - ) + assertUIObjectExists(itemWithText("Allow in private browsing"), waitingTime = waitingTimeLong) onView(withId(R.id.allow_in_private_browsing)).click() } - fun installAddon(addonName: String) { + fun installAddon(addonName: String, activityTestRule: HomeActivityIntentTestRule) { homeScreen { }.openThreeDotMenu { }.openAddonsManagerMenu { clickInstallAddon(addonName) verifyAddonPermissionPrompt(addonName) acceptPermissionToInstallAddon() + verifyAddonInstallCompleted(addonName, activityTestRule) } } @@ -212,11 +207,6 @@ class SettingsSubMenuAddonsManagerRobot { ), ) - private fun assertAddonIsEnabled(addonName: String) { - installButtonForAddon(addonName) - .check(matches(not(isCompletelyDisplayed()))) - } - private fun assertAddonIsInstalled(addonName: String) { onView( allOf( @@ -299,3 +289,6 @@ fun addonsMenu(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): Settings SettingsSubMenuAddonsManagerRobot().interact() return SettingsSubMenuAddonsManagerRobot.Transition() } + +private fun addonsList() = + UiScrollable(UiSelector().resourceId("$packageName:id/add_ons_list")).setAsVerticalList() diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAutofillRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAutofillRobot.kt index 19af89a5cc..67b82b283f 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAutofillRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAutofillRobot.kt @@ -4,6 +4,7 @@ package org.mozilla.fenix.ui.robots +import android.util.Log import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.RootMatchers @@ -16,16 +17,14 @@ import androidx.test.uiautomator.UiSelector import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.endsWith import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue import org.mozilla.fenix.R -import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithDescriptionExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists +import org.mozilla.fenix.helpers.Constants.TAG +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.hasCousin import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName @@ -34,11 +33,17 @@ import org.mozilla.fenix.helpers.click class SettingsSubMenuAutofillRobot { - fun verifyAutofillToolbarTitle() = assertItemContainingTextExists(autofillToolbarTitle) - fun verifyManageAddressesToolbarTitle() = assertItemContainingTextExists(manageAddressesToolbarTitle) + fun verifyAutofillToolbarTitle() { + assertUIObjectExists(autofillToolbarTitle) + Log.i(TAG, "verifyAutofillToolbarTitle: Verified \"Autofill\" toolbar title exists") + } + fun verifyManageAddressesToolbarTitle() { + assertUIObjectExists(manageAddressesToolbarTitle) + Log.i(TAG, "verifyManageAddressesToolbarTitle: Verified \"Manage addresses\" toolbar title exists") + } fun verifyAddressAutofillSection(isAddressAutofillEnabled: Boolean, userHasSavedAddress: Boolean) { - assertItemContainingTextExists( + assertUIObjectExists( autofillToolbarTitle, addressesSectionTitle, saveAndAutofillAddressesOption, @@ -46,16 +51,16 @@ class SettingsSubMenuAutofillRobot { ) if (userHasSavedAddress) { - assertItemContainingTextExists(manageAddressesButton) + assertUIObjectExists(manageAddressesButton) } else { - assertItemContainingTextExists(addAddressButton) + assertUIObjectExists(addAddressButton) } verifyAddressesAutofillToggle(isAddressAutofillEnabled) } fun verifyCreditCardsAutofillSection(isAddressAutofillEnabled: Boolean, userHasSavedCreditCard: Boolean) { - assertItemContainingTextExists( + assertUIObjectExists( autofillToolbarTitle, creditCardsSectionTitle, saveAndAutofillCreditCardsOption, @@ -65,32 +70,29 @@ class SettingsSubMenuAutofillRobot { ) if (userHasSavedCreditCard) { - assertItemContainingTextExists(manageSavedCreditCardsButton) + assertUIObjectExists(manageSavedCreditCardsButton) } else { - assertItemContainingTextExists(addCreditCardButton) + assertUIObjectExists(addCreditCardButton) } verifySaveAndAutofillCreditCardsToggle(isAddressAutofillEnabled) } fun verifyManageAddressesSection(vararg savedAddressDetails: String) { - assertItemWithDescriptionExists(navigateBackButton) - assertItemContainingTextExists( + assertUIObjectExists( + navigateBackButton, manageAddressesToolbarTitle, addAddressButton, ) for (savedAddressDetail in savedAddressDetails) { - assertTrue( - mDevice.findObject( - UiSelector().textContains(savedAddressDetail), - ).waitForExists(waitingTime), - ) + assertUIObjectExists(itemContainingText(savedAddressDetail)) + Log.i(TAG, "verifyManageAddressesSection: Verified saved address detail: $savedAddressDetail exists") } } fun verifySavedCreditCardsSection(creditCardLastDigits: String, creditCardExpiryDate: String) { - assertItemWithDescriptionExists(navigateBackButton) - assertItemContainingTextExists( + assertUIObjectExists( + navigateBackButton, savedCreditCardsToolbarTitle, addCreditCardButton, itemContainingText(creditCardLastDigits), @@ -98,7 +100,7 @@ class SettingsSubMenuAutofillRobot { ) } - fun verifyAddressesAutofillToggle(enabled: Boolean) = + fun verifyAddressesAutofillToggle(enabled: Boolean) { onView(withText(R.string.preferences_addresses_save_and_autofill_addresses)) .check( matches( @@ -114,6 +116,8 @@ class SettingsSubMenuAutofillRobot { ), ), ) + Log.i(TAG, "verifyAddressesAutofillToggle: Verified if address autofill toggle is enabled: $enabled") + } fun verifySaveAndAutofillCreditCardsToggle(enabled: Boolean) = onView(withText(R.string.preferences_credit_cards_save_and_autofill_cards)) @@ -133,31 +137,34 @@ class SettingsSubMenuAutofillRobot { ) fun verifyAddAddressView() { - assertItemContainingTextExists(addAddressToolbarTitle) - assertItemWithDescriptionExists(navigateBackButton) - assertItemWithResIdExists( + assertUIObjectExists( + addAddressToolbarTitle, + navigateBackButton, toolbarCheckmarkButton, firstNameTextInput, middleNameTextInput, ) scrollToElementByText(getStringResource(R.string.addresses_street_address)) - assertItemWithResIdExists( + Log.i(TAG, "verifyAddAddressView: Scrolled to \"Street Address\" text input") + assertUIObjectExists( lastNameTextInput, streetAddressTextInput, ) scrollToElementByText(getStringResource(R.string.addresses_country)) - assertItemWithResIdExists( + Log.i(TAG, "verifyAddAddressView: Scrolled to \"Country or region\" dropdown") + assertUIObjectExists( cityTextInput, subRegionDropDown, zipCodeTextInput, ) scrollToElementByText(getStringResource(R.string.addresses_save_button)) - assertItemWithResIdExists( + Log.i(TAG, "verifyAddAddressView: Scrolled to \"Save\" button") + assertUIObjectExists( countryDropDown, phoneTextInput, emailTextInput, ) - assertItemWithResIdExists( + assertUIObjectExists( saveButton, cancelButton, ) @@ -165,84 +172,124 @@ class SettingsSubMenuAutofillRobot { fun verifyCountryOption(country: String) { scrollToElementByText(getStringResource(R.string.addresses_country)) + Log.i(TAG, "verifyCountryOption: Scrolled to \"Country or region\" dropdown") mDevice.pressBack() - assertItemContainingTextExists(itemContainingText(country)) + Log.i(TAG, "fillAndSaveAddress: Dismissed \"Country or region\" dropdown using device back button") + assertUIObjectExists(itemContainingText(country)) } - fun verifyStateOption(state: String) = - assertItemContainingTextExists(itemContainingText(state)) + fun verifyStateOption(state: String) { + assertUIObjectExists(itemContainingText(state)) + } fun verifyCountryOptions(vararg countries: String) { countryDropDown.click() + Log.i(TAG, "verifyCountryOptions: Clicked \"Country or region\" dropdown") for (country in countries) { - assertItemContainingTextExists(itemContainingText(country)) + assertUIObjectExists(itemContainingText(country)) } } fun selectCountry(country: String) { countryDropDown.click() + Log.i(TAG, "selectCountry: Clicked \"Country or region\" dropdown") countryOption(country).click() + Log.i(TAG, "selectCountry: Selected $country dropdown option") } fun verifyEditAddressView() { - assertItemContainingTextExists(editAddressToolbarTitle) - assertItemWithDescriptionExists(navigateBackButton) - assertItemWithResIdExists( + assertUIObjectExists( + editAddressToolbarTitle, + navigateBackButton, toolbarDeleteAddressButton, toolbarCheckmarkButton, firstNameTextInput, middleNameTextInput, ) scrollToElementByText(getStringResource(R.string.addresses_street_address)) - assertItemWithResIdExists( + Log.i(TAG, "verifyEditAddressView: Scrolled to \"Street Address\" text input") + assertUIObjectExists( lastNameTextInput, streetAddressTextInput, ) scrollToElementByText(getStringResource(R.string.addresses_country)) - assertItemWithResIdExists( + Log.i(TAG, "verifyEditAddressView: Scrolled to \"Country or region\" dropdown") + assertUIObjectExists( cityTextInput, subRegionDropDown, zipCodeTextInput, ) scrollToElementByText(getStringResource(R.string.addresses_save_button)) - assertItemWithResIdExists( + Log.i(TAG, "verifyEditAddressView: Scrolled to \"Save\" button") + assertUIObjectExists( countryDropDown, phoneTextInput, emailTextInput, ) - assertItemWithResIdExists( + assertUIObjectExists( saveButton, cancelButton, ) - assertItemContainingTextExists(deleteAddressButton) + assertUIObjectExists(deleteAddressButton) } - fun clickSaveAndAutofillAddressesOption() = saveAndAutofillAddressesOption.click() - fun clickAddAddressButton() = addAddressButton.click() - fun clickManageAddressesButton() = manageAddressesButton.click() - fun clickSavedAddress(firstName: String) = savedAddress(firstName).clickAndWaitForNewWindow(waitingTime) + fun clickSaveAndAutofillAddressesOption() { + saveAndAutofillAddressesOption.click() + Log.i(TAG, "clickSaveAndAutofillAddressesOption: Clicked \"Save and autofill addresses\" button") + } + fun clickAddAddressButton() { + addAddressButton.click() + Log.i(TAG, "clickAddAddressButton: Clicked \"Add address\" button") + } + fun clickManageAddressesButton() { + manageAddressesButton.click() + Log.i(TAG, "clickManageAddressesButton: Clicked \"Manage addresses\" button") + } + fun clickSavedAddress(firstName: String) { + savedAddress(firstName).clickAndWaitForNewWindow(waitingTime) + Log.i(TAG, "clickSavedAddress: Clicked $firstName saved address and waiting for a new window for $waitingTime") + } fun clickDeleteAddressButton() { + Log.i(TAG, "clickDeleteAddressButton: Looking for delete address toolbar button") toolbarDeleteAddressButton.waitForExists(waitingTime) toolbarDeleteAddressButton.click() + Log.i(TAG, "clickDeleteAddressButton: Clicked delete address toolbar button") + } + fun clickCancelDeleteAddressButton() { + cancelDeleteAddressButton.click() + Log.i(TAG, "clickCancelDeleteAddressButton: Clicked \"CANCEL\" button from delete address dialog") } - fun clickCancelDeleteAddressButton() = cancelDeleteAddressButton.click() - fun clickConfirmDeleteAddressButton() = confirmDeleteAddressButton.click() + fun clickConfirmDeleteAddressButton() { + confirmDeleteAddressButton.click() + Log.i(TAG, "clickConfirmDeleteAddressButton: Clicked \"DELETE\" button from delete address dialog") + } fun clickSubRegionOption(subRegion: String) { scrollToElementByText(subRegion) + Log.i(TAG, "clickSubRegionOption: Scrolled to \"State\" drop down") subRegionOption(subRegion).also { + Log.i(TAG, "clickSubRegionOption: Looking for \"State\" $subRegion dropdown option") it.waitForExists(waitingTime) it.click() + Log.i(TAG, "clickSubRegionOption: Clicked \"State\" $subRegion dropdown option") } } fun clickCountryOption(country: String) { + Log.i(TAG, "clickCountryOption: Looking for \"Country or region\" $country dropdown option") countryOption(country).waitForExists(waitingTime) countryOption(country).click() + Log.i(TAG, "clickCountryOption: Clicked \"Country or region\" $country dropdown option") + } + fun verifyAddAddressButton() { + assertUIObjectExists(addAddressButton) + Log.i(TAG, "verifyAddAddressButton: Verified \"Add address\" button exists") } - fun verifyAddAddressButton() = assertTrue(addAddressButton.waitForExists(waitingTime)) fun fillAndSaveAddress( + navigateToAutofillSettings: Boolean, + isAddressAutofillEnabled: Boolean = true, + userHasSavedAddress: Boolean = false, firstName: String, middleName: String, lastName: String, @@ -254,22 +301,48 @@ class SettingsSubMenuAutofillRobot { phoneNumber: String, emailAddress: String, ) { + if (navigateToAutofillSettings) { + homeScreen { + }.openThreeDotMenu { + }.openSettings { + }.openAutofillSubMenu { + verifyAddressAutofillSection(isAddressAutofillEnabled, userHasSavedAddress) + clickAddAddressButton() + } + } + Log.i(TAG, "fillAndSaveAddress: Looking for \"First Name\" text input") firstNameTextInput.waitForExists(waitingTime) mDevice.pressBack() + Log.i(TAG, "fillAndSaveAddress: Dismissed keyboard using device back button") firstNameTextInput.setText(firstName) + Log.i(TAG, "fillAndSaveAddress: \"First Name\" set to $firstName") middleNameTextInput.setText(middleName) + Log.i(TAG, "fillAndSaveAddress: \"Middle Name\" set to $middleName") lastNameTextInput.setText(lastName) + Log.i(TAG, "fillAndSaveAddress: \"Last Name\" set to $lastName") streetAddressTextInput.setText(streetAddress) + Log.i(TAG, "fillAndSaveAddress: \"Street Address\" set to $streetAddress") cityTextInput.setText(city) + Log.i(TAG, "fillAndSaveAddress: \"City\" set to $city") subRegionDropDown.click() + Log.i(TAG, "fillAndSaveAddress: Clicked \"State\" dropdown button") clickSubRegionOption(state) + Log.i(TAG, "fillAndSaveAddress: Selected $state as \"State\"") zipCodeTextInput.setText(zipCode) + Log.i(TAG, "fillAndSaveAddress: \"Zip\" set to $zipCode") countryDropDown.click() + Log.i(TAG, "fillAndSaveAddress: Clicked \"Country or region\" dropdown button") clickCountryOption(country) + Log.i(TAG, "fillAndSaveAddress: Selected $country as \"Country or region\"") scrollToElementByText(getStringResource(R.string.addresses_save_button)) + Log.i(TAG, "fillAndSaveAddress: Scrolled to \"Save\" button") phoneTextInput.setText(phoneNumber) + Log.i(TAG, "fillAndSaveAddress: \"Phone\" set to $phoneNumber") emailTextInput.setText(emailAddress) + Log.i(TAG, "fillAndSaveAddress: \"Email\" set to $emailAddress") saveButton.click() + Log.i(TAG, "fillAndSaveAddress: Clicked \"Save\" button") + Log.i(TAG, "fillAndSaveAddress: Looking for \"Manage addressese\" button") manageAddressesButton.waitForExists(waitingTime) } @@ -301,7 +374,7 @@ class SettingsSubMenuAutofillRobot { expiryYearOption(expiryYear).click() } - fun verifyAddCreditCardsButton() = assertTrue(addCreditCardButton.waitForExists(waitingTime)) + fun verifyAddCreditCardsButton() = assertUIObjectExists(addCreditCardButton) fun fillAndSaveCreditCard(cardNumber: String, cardName: String, expiryMonth: String, expiryYear: String) { creditCardNumberTextInput.waitForExists(waitingTime) @@ -336,10 +409,9 @@ class SettingsSubMenuAutofillRobot { expiryMonth: String, expiryYear: String, ) { - assertItemContainingTextExists(editCreditCardToolbarTitle) - assertItemWithDescriptionExists(navigateBackButton) - - assertItemWithResIdExists( + assertUIObjectExists( + editCreditCardToolbarTitle, + navigateBackButton, deleteCreditCardToolbarButton, saveCreditCardToolbarButton, ) @@ -348,31 +420,31 @@ class SettingsSubMenuAutofillRobot { assertEquals(cardName, nameOnCreditCardTextInput.text) // Can't get the text from the drop-down items, need to verify them individually - assertItemWithResIdExists( + assertUIObjectExists( expiryYearDropDown, expiryMonthDropDown, ) - assertItemContainingTextExists( + assertUIObjectExists( itemContainingText(expiryMonth), itemContainingText(expiryYear), ) - assertItemWithResIdExists( + assertUIObjectExists( saveButton, cancelButton, ) - assertItemContainingTextExists(deleteCreditCardMenuButton) + assertUIObjectExists(deleteCreditCardMenuButton) } - fun verifyEditCreditCardToolbarTitle() = assertItemContainingTextExists(editCreditCardToolbarTitle) + fun verifyEditCreditCardToolbarTitle() = assertUIObjectExists(editCreditCardToolbarTitle) fun verifyCreditCardNumberErrorMessage() = - assertItemContainingTextExists(itemContainingText(getStringResource(R.string.credit_cards_number_validation_error_message))) + assertUIObjectExists(itemContainingText(getStringResource(R.string.credit_cards_number_validation_error_message))) fun verifyNameOnCreditCardErrorMessage() = - assertItemContainingTextExists(itemContainingText(getStringResource(R.string.credit_cards_name_on_card_validation_error_message))) + assertUIObjectExists(itemContainingText(getStringResource(R.string.credit_cards_name_on_card_validation_error_message))) class Transition { fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition { @@ -384,6 +456,7 @@ class SettingsSubMenuAutofillRobot { fun goBackToAutofillSettings(interact: SettingsSubMenuAutofillRobot.() -> Unit): SettingsSubMenuAutofillRobot.Transition { navigateBackButton.click() + Log.i(TAG, "goBackToAutofillSettings: Clicked \"Navigate back\" toolbar button") SettingsSubMenuAutofillRobot().interact() return SettingsSubMenuAutofillRobot.Transition() @@ -398,6 +471,7 @@ class SettingsSubMenuAutofillRobot { fun goBackToBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { mDevice.pressBack() + Log.i(TAG, "goBackToBrowser: Go back to browser view using device back button") BrowserRobot().interact() return BrowserRobot.Transition() @@ -405,6 +479,11 @@ class SettingsSubMenuAutofillRobot { } } +fun autofillScreen(interact: SettingsSubMenuAutofillRobot.() -> Unit): SettingsSubMenuAutofillRobot.Transition { + SettingsSubMenuAutofillRobot().interact() + return SettingsSubMenuAutofillRobot.Transition() +} + private val autofillToolbarTitle = itemContainingText(getStringResource(R.string.preferences_autofill)) private val addressesSectionTitle = itemContainingText(getStringResource(R.string.preferences_addresses)) private val manageAddressesToolbarTitle = diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuCookieBannerReductionRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuCookieBannerReductionRobot.kt deleted file mode 100644 index 482050fb79..0000000000 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuCookieBannerReductionRobot.kt +++ /dev/null @@ -1,47 +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.robots - -import org.mozilla.fenix.R -import org.mozilla.fenix.helpers.MatcherHelper.assertCheckedItemWithResIdExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists -import org.mozilla.fenix.helpers.MatcherHelper.checkedItemWithResId -import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText -import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId -import org.mozilla.fenix.helpers.TestHelper.getStringResource -import org.mozilla.fenix.helpers.TestHelper.mDevice -import org.mozilla.fenix.helpers.TestHelper.packageName -import org.mozilla.fenix.helpers.click - -/** - * Implementation of Robot Pattern for the settings Cookie Banner Reduction sub menu. - */ -class SettingsSubMenuCookieBannerReductionRobot { - fun verifyCookieBannerView(isCookieBannerReductionChecked: Boolean) { - assertItemContainingTextExists(cookieBannerOptionTitle, cookieBannerOptionDescription) - assertCheckedItemWithResIdExists(checkedCookieBannerOptionToggle(isCookieBannerReductionChecked)) - } - fun clickCookieBannerReductionToggle() = cookieBannerOptionToggle.click() - fun verifyCheckedCookieBannerReductionToggle(isCookieBannerReductionChecked: Boolean) = - assertCheckedItemWithResIdExists(checkedCookieBannerOptionToggle(isCookieBannerReductionChecked)) - - class Transition { - fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition { - mDevice.pressBack() - - SettingsRobot().interact() - return SettingsRobot.Transition() - } - } -} - -private val cookieBannerOptionTitle = - itemContainingText(getStringResource(R.string.reduce_cookie_banner_option)) -private val cookieBannerOptionDescription = - itemContainingText(getStringResource(R.string.reduce_cookie_banner_summary_1)) -private val cookieBannerOptionToggle = - itemWithResId("$packageName:id/learn_more_switch") -private fun checkedCookieBannerOptionToggle(isChecked: Boolean = false) = - checkedItemWithResId("$packageName:id/learn_more_switch", isChecked) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuCustomizeRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuCustomizeRobot.kt index 24235e1796..20d455d197 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuCustomizeRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuCustomizeRobot.kt @@ -21,7 +21,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import org.hamcrest.CoreMatchers.allOf import org.hamcrest.Matchers.endsWith import org.mozilla.fenix.R -import org.mozilla.fenix.helpers.TestHelper.getStringResource +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.hasCousin import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.click diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDataCollectionRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDataCollectionRobot.kt index 855cb0ccfa..062d3a05cf 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDataCollectionRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDataCollectionRobot.kt @@ -1,142 +1,140 @@ -/* 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.robots - -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.RootMatchers -import androidx.test.espresso.matcher.ViewMatchers -import androidx.test.espresso.matcher.ViewMatchers.isChecked -import androidx.test.espresso.matcher.ViewMatchers.isNotChecked -import androidx.test.espresso.matcher.ViewMatchers.withClassName -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.hamcrest.CoreMatchers.endsWith -import org.mozilla.fenix.R -import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithDescriptionExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists -import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText -import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription -import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId -import org.mozilla.fenix.helpers.TestHelper.getStringResource -import org.mozilla.fenix.helpers.TestHelper.hasCousin -import org.mozilla.fenix.helpers.TestHelper.packageName -import org.mozilla.fenix.helpers.click - -/** - * Implementation of Robot Pattern for the settings Data Collection sub menu. - */ -class SettingsSubMenuDataCollectionRobot { - - fun verifyDataCollectionView( - isUsageAndTechnicalDataEnabled: Boolean, - isMarketingDataEnabled: Boolean, - studiesSummary: String, - ) { - assertItemWithDescriptionExists(goBackButton()) - assertItemContainingTextExists( - itemContainingText(getStringResource(R.string.preferences_data_collection)), - itemContainingText(getStringResource(R.string.preference_usage_data)), - itemContainingText(getStringResource(R.string.preferences_usage_data_description)), - ) - verifyUsageAndTechnicalDataToggle(isUsageAndTechnicalDataEnabled) - assertItemContainingTextExists( - itemContainingText(getStringResource(R.string.preferences_marketing_data)), - itemContainingText(getStringResource(R.string.preferences_marketing_data_description2)), - ) - verifyMarketingDataToggle(isMarketingDataEnabled) - assertItemContainingTextExists( - itemContainingText(getStringResource(R.string.preference_experiments_2)), - itemContainingText(studiesSummary), - ) - } - - fun verifyUsageAndTechnicalDataToggle(enabled: Boolean) = - onView(withText(R.string.preference_usage_data)) - .check( - matches( - hasCousin( - allOf( - withClassName(endsWith("Switch")), - if (enabled) { - isChecked() - } else { - isNotChecked() - }, - ), - ), - ), - ) - - fun verifyMarketingDataToggle(enabled: Boolean) = - onView(withText(R.string.preferences_marketing_data)) - .check( - matches( - hasCousin( - allOf( - withClassName(endsWith("Switch")), - if (enabled) { - isChecked() - } else { - isNotChecked() - }, - ), - ), - ), - ) - - fun verifyStudiesToggle(enabled: Boolean) = - onView(withId(R.id.studies_switch)) - .check( - matches( - if (enabled) { - isChecked() - } else { - isNotChecked() - }, - ), - ) - - fun clickUsageAndTechnicalDataToggle() = - itemContainingText(getStringResource(R.string.preference_usage_data)).click() - - fun clickMarketingDataToggle() = - itemContainingText(getStringResource(R.string.preferences_marketing_data)).click() - - fun clickStudiesOption() = - itemContainingText(getStringResource(R.string.preference_experiments_2)).click() - - fun clickStudiesToggle() = - itemWithResId("$packageName:id/studies_switch").click() - - fun verifyStudiesDialog() { - assertItemWithResIdExists(itemWithResId("$packageName:id/alertTitle")) - assertItemContainingTextExists( - itemContainingText(getStringResource(R.string.studies_restart_app)), - ) - studiesDialogOkButton.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) - studiesDialogCancelButton.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) - } - - fun clickStudiesDialogCancelButton() = studiesDialogCancelButton.click() - - fun clickStudiesDialogOkButton() = studiesDialogOkButton.click() - - class Transition { - fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition { - goBackButton().click() - - SettingsRobot().interact() - return SettingsRobot.Transition() - } - } -} - -private fun goBackButton() = itemWithDescription("Navigate up") -private val studiesDialogOkButton = onView(withId(android.R.id.button1)).inRoot(RootMatchers.isDialog()) -private val studiesDialogCancelButton = onView(withId(android.R.id.button2)).inRoot(RootMatchers.isDialog()) +/* 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.robots + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import androidx.test.espresso.matcher.ViewMatchers.isNotChecked +import androidx.test.espresso.matcher.ViewMatchers.withClassName +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.hamcrest.CoreMatchers.endsWith +import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText +import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription +import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId +import org.mozilla.fenix.helpers.TestHelper.hasCousin +import org.mozilla.fenix.helpers.TestHelper.packageName +import org.mozilla.fenix.helpers.click + +/** + * Implementation of Robot Pattern for the settings Data Collection sub menu. + */ +class SettingsSubMenuDataCollectionRobot { + + fun verifyDataCollectionView( + isUsageAndTechnicalDataEnabled: Boolean, + isMarketingDataEnabled: Boolean, + studiesSummary: String, + ) { + assertUIObjectExists( + goBackButton(), + itemContainingText(getStringResource(R.string.preferences_data_collection)), + itemContainingText(getStringResource(R.string.preference_usage_data)), + itemContainingText(getStringResource(R.string.preferences_usage_data_description)), + ) + verifyUsageAndTechnicalDataToggle(isUsageAndTechnicalDataEnabled) + assertUIObjectExists( + itemContainingText(getStringResource(R.string.preferences_marketing_data)), + itemContainingText(getStringResource(R.string.preferences_marketing_data_description2)), + ) + verifyMarketingDataToggle(isMarketingDataEnabled) + assertUIObjectExists( + itemContainingText(getStringResource(R.string.preference_experiments_2)), + itemContainingText(studiesSummary), + ) + } + + fun verifyUsageAndTechnicalDataToggle(enabled: Boolean) = + onView(withText(R.string.preference_usage_data)) + .check( + matches( + hasCousin( + allOf( + withClassName(endsWith("Switch")), + if (enabled) { + isChecked() + } else { + isNotChecked() + }, + ), + ), + ), + ) + + fun verifyMarketingDataToggle(enabled: Boolean) = + onView(withText(R.string.preferences_marketing_data)) + .check( + matches( + hasCousin( + allOf( + withClassName(endsWith("Switch")), + if (enabled) { + isChecked() + } else { + isNotChecked() + }, + ), + ), + ), + ) + + fun verifyStudiesToggle(enabled: Boolean) = + onView(withId(R.id.studies_switch)) + .check( + matches( + if (enabled) { + isChecked() + } else { + isNotChecked() + }, + ), + ) + + fun clickUsageAndTechnicalDataToggle() = + itemContainingText(getStringResource(R.string.preference_usage_data)).click() + + fun clickMarketingDataToggle() = + itemContainingText(getStringResource(R.string.preferences_marketing_data)).click() + + fun clickStudiesOption() = + itemContainingText(getStringResource(R.string.preference_experiments_2)).click() + + fun clickStudiesToggle() = + itemWithResId("$packageName:id/studies_switch").click() + + fun verifyStudiesDialog() { + assertUIObjectExists( + itemWithResId("$packageName:id/alertTitle"), + itemContainingText(getStringResource(R.string.studies_restart_app)), + ) + studiesDialogOkButton.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) + studiesDialogCancelButton.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) + } + + fun clickStudiesDialogCancelButton() = studiesDialogCancelButton.click() + + fun clickStudiesDialogOkButton() = studiesDialogOkButton.click() + + class Transition { + fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition { + goBackButton().click() + + SettingsRobot().interact() + return SettingsRobot.Transition() + } + } +} + +private fun goBackButton() = itemWithDescription("Navigate up") +private val studiesDialogOkButton = onView(withId(android.R.id.button1)).inRoot(RootMatchers.isDialog()) +private val studiesDialogCancelButton = onView(withId(android.R.id.button2)).inRoot(RootMatchers.isDialog()) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDeleteBrowsingDataRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDeleteBrowsingDataRobot.kt index 70ead50ae8..21978b4a39 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDeleteBrowsingDataRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDeleteBrowsingDataRobot.kt @@ -1,215 +1,210 @@ -/* 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.robots - -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.RootMatchers.isDialog -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 androidx.test.uiautomator.UiSelector -import org.hamcrest.CoreMatchers.allOf -import org.junit.Assert.assertTrue -import org.mozilla.fenix.R -import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime -import org.mozilla.fenix.helpers.TestHelper.appName -import org.mozilla.fenix.helpers.TestHelper.mDevice -import org.mozilla.fenix.helpers.assertIsChecked -import org.mozilla.fenix.helpers.click - -/** - * Implementation of Robot Pattern for the settings Delete Browsing Data sub menu. - */ -class SettingsSubMenuDeleteBrowsingDataRobot { - - fun verifyAllCheckBoxesAreChecked() = assertAllCheckBoxesAreChecked() - fun verifyOpenTabsCheckBox(status: Boolean) = assertOpenTabsCheckBox(status) - fun verifyBrowsingHistoryDetails(status: Boolean) = assertBrowsingHistoryCheckBox(status) - fun verifyCookiesCheckBox(status: Boolean) = assertCookiesCheckBox(status) - fun verifyCachedFilesCheckBox(status: Boolean) = assertCachedFilesCheckBox(status) - fun verifySitePermissionsCheckBox(status: Boolean) = assertSitePermissionsCheckBox(status) - fun verifyDownloadsCheckBox(status: Boolean) = assertDownloadsCheckBox(status) - fun verifyOpenTabsDetails(tabNumber: String) = assertOpenTabsDescription(tabNumber) - fun verifyBrowsingHistoryDetails(addresses: String) = assertBrowsingHistoryDescription(addresses) - - fun verifyDeleteBrowsingDataDialog() { - dialogMessage().check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - dialogCancelButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - dialogDeleteButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - } - - fun switchOpenTabsCheckBox() = clickOpenTabsCheckBox() - fun switchBrowsingHistoryCheckBox() = clickBrowsingHistoryCheckBox() - fun switchCookiesCheckBox() = clickCookiesCheckBox() - fun switchCachedFilesCheckBox() = clickCachedFilesCheckBox() - fun switchSitePermissionsCheckBox() = clickSitePermissionsCheckBox() - fun switchDownloadsCheckBox() = clickDownloadsCheckBox() - fun clickDeleteBrowsingDataButton() = deleteBrowsingDataButton().click() - fun clickDialogCancelButton() = dialogCancelButton().click() - - fun selectOnlyOpenTabsCheckBox() { - clickBrowsingHistoryCheckBox() - assertBrowsingHistoryCheckBox(false) - - clickCookiesCheckBox() - assertCookiesCheckBox(false) - - clickCachedFilesCheckBox() - assertCachedFilesCheckBox(false) - - clickSitePermissionsCheckBox() - assertSitePermissionsCheckBox(false) - - clickDownloadsCheckBox() - assertDownloadsCheckBox(false) - - assertOpenTabsCheckBox(true) - } - - fun selectOnlyBrowsingHistoryCheckBox() { - clickOpenTabsCheckBox() - assertOpenTabsCheckBox(false) - - clickCookiesCheckBox() - assertCookiesCheckBox(false) - - clickCachedFilesCheckBox() - assertCachedFilesCheckBox(false) - - clickSitePermissionsCheckBox() - assertSitePermissionsCheckBox(false) - - clickDownloadsCheckBox() - assertDownloadsCheckBox(false) - - assertBrowsingHistoryCheckBox(true) - } - - fun selectOnlyCookiesCheckBox() { - clickOpenTabsCheckBox() - assertOpenTabsCheckBox(false) - - assertCookiesCheckBox(true) - - clickCachedFilesCheckBox() - assertCachedFilesCheckBox(false) - - clickSitePermissionsCheckBox() - assertSitePermissionsCheckBox(false) - - clickDownloadsCheckBox() - assertDownloadsCheckBox(false) - - clickBrowsingHistoryCheckBox() - assertBrowsingHistoryCheckBox(false) - } - - fun selectOnlyCachedFilesCheckBox() { - clickOpenTabsCheckBox() - assertOpenTabsCheckBox(false) - - clickBrowsingHistoryCheckBox() - assertBrowsingHistoryCheckBox(false) - - clickCookiesCheckBox() - assertCookiesCheckBox(false) - - assertCachedFilesCheckBox(true) - - clickSitePermissionsCheckBox() - assertSitePermissionsCheckBox(false) - - clickDownloadsCheckBox() - assertDownloadsCheckBox(false) - } - - fun confirmDeletionAndAssertSnackbar() { - dialogDeleteButton().click() - assertDeleteBrowsingDataSnackbar() - } - - class Transition { - fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition { - goBackButton().click() - - SettingsRobot().interact() - return SettingsRobot.Transition() - } - } -} - -private fun goBackButton() = - onView(allOf(withContentDescription("Navigate up"))) - -private fun deleteBrowsingDataButton() = onView(withId(R.id.delete_data)) - -private fun dialogDeleteButton() = onView(withText("Delete")).inRoot(isDialog()) - -private fun dialogCancelButton() = onView(withText("Cancel")).inRoot(isDialog()) - -private fun openTabsDescription(tabNumber: String) = onView(withText("$tabNumber tabs")) - -private fun openTabsCheckBox() = onView(allOf(withId(R.id.checkbox), hasSibling(withText("Open tabs")))) - -private fun browsingHistoryDescription(addresses: String) = mDevice.findObject(UiSelector().textContains("$addresses addresses")) - -private fun browsingHistoryCheckBox() = - onView(allOf(withId(R.id.checkbox), hasSibling(withText("Browsing history")))) - -private fun cookiesAndSiteDataCheckBox() = - onView(allOf(withId(R.id.checkbox), hasSibling(withText("Cookies and site data")))) - -private fun cachedFilesCheckBox() = - onView(allOf(withId(R.id.checkbox), hasSibling(withText("Cached images and files")))) - -private fun sitePermissionsCheckBox() = - onView(allOf(withId(R.id.checkbox), hasSibling(withText("Site permissions")))) - -private fun downloadsCheckBox() = - onView(allOf(withId(R.id.checkbox), hasSibling(withText("Downloads")))) - -private fun dialogMessage() = - onView(withText("$appName will delete the selected browsing data.")) - .inRoot(isDialog()) - -private fun assertAllCheckBoxesAreChecked() { - openTabsCheckBox().assertIsChecked(true) - browsingHistoryCheckBox().assertIsChecked(true) - cookiesAndSiteDataCheckBox().assertIsChecked(true) - cachedFilesCheckBox().assertIsChecked(true) - sitePermissionsCheckBox().assertIsChecked(true) - downloadsCheckBox().assertIsChecked(true) -} - -private fun assertOpenTabsDescription(tabNumber: String) = - openTabsDescription(tabNumber).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - -private fun assertBrowsingHistoryDescription(addresses: String) = - assertTrue(browsingHistoryDescription(addresses).waitForExists(waitingTime)) - -private fun assertDeleteBrowsingDataSnackbar() { - assertTrue( - mDevice.findObject( - UiSelector().text("Browsing data deleted"), - ).waitUntilGone(waitingTime), - ) -} - -private fun clickOpenTabsCheckBox() = openTabsCheckBox().click() -private fun assertOpenTabsCheckBox(status: Boolean) = openTabsCheckBox().assertIsChecked(status) -private fun clickBrowsingHistoryCheckBox() = browsingHistoryCheckBox().click() -private fun assertBrowsingHistoryCheckBox(status: Boolean) = browsingHistoryCheckBox().assertIsChecked(status) -private fun clickCookiesCheckBox() = cookiesAndSiteDataCheckBox().click() -private fun assertCookiesCheckBox(status: Boolean) = cookiesAndSiteDataCheckBox().assertIsChecked(status) -private fun clickCachedFilesCheckBox() = cachedFilesCheckBox().click() -private fun assertCachedFilesCheckBox(status: Boolean) = cachedFilesCheckBox().assertIsChecked(status) -private fun clickSitePermissionsCheckBox() = sitePermissionsCheckBox().click() -private fun assertSitePermissionsCheckBox(status: Boolean) = sitePermissionsCheckBox().assertIsChecked(status) -private fun clickDownloadsCheckBox() = downloadsCheckBox().click() -private fun assertDownloadsCheckBox(status: Boolean) = downloadsCheckBox().assertIsChecked(status) +/* 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.robots + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.isDialog +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 androidx.test.uiautomator.UiSelector +import org.hamcrest.CoreMatchers.allOf +import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectIsGone +import org.mozilla.fenix.helpers.MatcherHelper.itemWithText +import org.mozilla.fenix.helpers.TestHelper.appName +import org.mozilla.fenix.helpers.TestHelper.mDevice +import org.mozilla.fenix.helpers.assertIsChecked +import org.mozilla.fenix.helpers.click + +/** + * Implementation of Robot Pattern for the settings Delete Browsing Data sub menu. + */ +class SettingsSubMenuDeleteBrowsingDataRobot { + + fun verifyAllCheckBoxesAreChecked() = assertAllCheckBoxesAreChecked() + fun verifyOpenTabsCheckBox(status: Boolean) = assertOpenTabsCheckBox(status) + fun verifyBrowsingHistoryDetails(status: Boolean) = assertBrowsingHistoryCheckBox(status) + fun verifyCookiesCheckBox(status: Boolean) = assertCookiesCheckBox(status) + fun verifyCachedFilesCheckBox(status: Boolean) = assertCachedFilesCheckBox(status) + fun verifySitePermissionsCheckBox(status: Boolean) = assertSitePermissionsCheckBox(status) + fun verifyDownloadsCheckBox(status: Boolean) = assertDownloadsCheckBox(status) + fun verifyOpenTabsDetails(tabNumber: String) = assertOpenTabsDescription(tabNumber) + fun verifyBrowsingHistoryDetails(addresses: String) = assertBrowsingHistoryDescription(addresses) + + fun verifyDeleteBrowsingDataDialog() { + dialogMessage().check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + dialogCancelButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + dialogDeleteButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + } + + fun switchOpenTabsCheckBox() = clickOpenTabsCheckBox() + fun switchBrowsingHistoryCheckBox() = clickBrowsingHistoryCheckBox() + fun switchCookiesCheckBox() = clickCookiesCheckBox() + fun switchCachedFilesCheckBox() = clickCachedFilesCheckBox() + fun switchSitePermissionsCheckBox() = clickSitePermissionsCheckBox() + fun switchDownloadsCheckBox() = clickDownloadsCheckBox() + fun clickDeleteBrowsingDataButton() = deleteBrowsingDataButton().click() + fun clickDialogCancelButton() = dialogCancelButton().click() + + fun selectOnlyOpenTabsCheckBox() { + clickBrowsingHistoryCheckBox() + assertBrowsingHistoryCheckBox(false) + + clickCookiesCheckBox() + assertCookiesCheckBox(false) + + clickCachedFilesCheckBox() + assertCachedFilesCheckBox(false) + + clickSitePermissionsCheckBox() + assertSitePermissionsCheckBox(false) + + clickDownloadsCheckBox() + assertDownloadsCheckBox(false) + + assertOpenTabsCheckBox(true) + } + + fun selectOnlyBrowsingHistoryCheckBox() { + clickOpenTabsCheckBox() + assertOpenTabsCheckBox(false) + + clickCookiesCheckBox() + assertCookiesCheckBox(false) + + clickCachedFilesCheckBox() + assertCachedFilesCheckBox(false) + + clickSitePermissionsCheckBox() + assertSitePermissionsCheckBox(false) + + clickDownloadsCheckBox() + assertDownloadsCheckBox(false) + + assertBrowsingHistoryCheckBox(true) + } + + fun selectOnlyCookiesCheckBox() { + clickOpenTabsCheckBox() + assertOpenTabsCheckBox(false) + + assertCookiesCheckBox(true) + + clickCachedFilesCheckBox() + assertCachedFilesCheckBox(false) + + clickSitePermissionsCheckBox() + assertSitePermissionsCheckBox(false) + + clickDownloadsCheckBox() + assertDownloadsCheckBox(false) + + clickBrowsingHistoryCheckBox() + assertBrowsingHistoryCheckBox(false) + } + + fun selectOnlyCachedFilesCheckBox() { + clickOpenTabsCheckBox() + assertOpenTabsCheckBox(false) + + clickBrowsingHistoryCheckBox() + assertBrowsingHistoryCheckBox(false) + + clickCookiesCheckBox() + assertCookiesCheckBox(false) + + assertCachedFilesCheckBox(true) + + clickSitePermissionsCheckBox() + assertSitePermissionsCheckBox(false) + + clickDownloadsCheckBox() + assertDownloadsCheckBox(false) + } + + fun confirmDeletionAndAssertSnackbar() { + dialogDeleteButton().click() + assertDeleteBrowsingDataSnackbar() + } + + class Transition { + fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition { + goBackButton().click() + + SettingsRobot().interact() + return SettingsRobot.Transition() + } + } +} + +private fun goBackButton() = + onView(allOf(withContentDescription("Navigate up"))) + +private fun deleteBrowsingDataButton() = onView(withId(R.id.delete_data)) + +private fun dialogDeleteButton() = onView(withText("Delete")).inRoot(isDialog()) + +private fun dialogCancelButton() = onView(withText("Cancel")).inRoot(isDialog()) + +private fun openTabsDescription(tabNumber: String) = onView(withText("$tabNumber tabs")) + +private fun openTabsCheckBox() = onView(allOf(withId(R.id.checkbox), hasSibling(withText("Open tabs")))) + +private fun browsingHistoryDescription(addresses: String) = mDevice.findObject(UiSelector().textContains("$addresses addresses")) + +private fun browsingHistoryCheckBox() = + onView(allOf(withId(R.id.checkbox), hasSibling(withText("Browsing history")))) + +private fun cookiesAndSiteDataCheckBox() = + onView(allOf(withId(R.id.checkbox), hasSibling(withText("Cookies and site data")))) + +private fun cachedFilesCheckBox() = + onView(allOf(withId(R.id.checkbox), hasSibling(withText("Cached images and files")))) + +private fun sitePermissionsCheckBox() = + onView(allOf(withId(R.id.checkbox), hasSibling(withText("Site permissions")))) + +private fun downloadsCheckBox() = + onView(allOf(withId(R.id.checkbox), hasSibling(withText("Downloads")))) + +private fun dialogMessage() = + onView(withText("$appName will delete the selected browsing data.")) + .inRoot(isDialog()) + +private fun assertAllCheckBoxesAreChecked() { + openTabsCheckBox().assertIsChecked(true) + browsingHistoryCheckBox().assertIsChecked(true) + cookiesAndSiteDataCheckBox().assertIsChecked(true) + cachedFilesCheckBox().assertIsChecked(true) + sitePermissionsCheckBox().assertIsChecked(true) + downloadsCheckBox().assertIsChecked(true) +} + +private fun assertOpenTabsDescription(tabNumber: String) = + openTabsDescription(tabNumber).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + +private fun assertBrowsingHistoryDescription(addresses: String) = + assertUIObjectExists(browsingHistoryDescription(addresses)) + +private fun assertDeleteBrowsingDataSnackbar() = assertUIObjectIsGone(itemWithText("Browsing data deleted")) + +private fun clickOpenTabsCheckBox() = openTabsCheckBox().click() +private fun assertOpenTabsCheckBox(status: Boolean) = openTabsCheckBox().assertIsChecked(status) +private fun clickBrowsingHistoryCheckBox() = browsingHistoryCheckBox().click() +private fun assertBrowsingHistoryCheckBox(status: Boolean) = browsingHistoryCheckBox().assertIsChecked(status) +private fun clickCookiesCheckBox() = cookiesAndSiteDataCheckBox().click() +private fun assertCookiesCheckBox(status: Boolean) = cookiesAndSiteDataCheckBox().assertIsChecked(status) +private fun clickCachedFilesCheckBox() = cachedFilesCheckBox().click() +private fun assertCachedFilesCheckBox(status: Boolean) = cachedFilesCheckBox().assertIsChecked(status) +private fun clickSitePermissionsCheckBox() = sitePermissionsCheckBox().click() +private fun assertSitePermissionsCheckBox(status: Boolean) = sitePermissionsCheckBox().assertIsChecked(status) +private fun clickDownloadsCheckBox() = downloadsCheckBox().click() +private fun assertDownloadsCheckBox(status: Boolean) = downloadsCheckBox().assertIsChecked(status) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot.kt index 6bd003e928..db0486f71d 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot.kt @@ -14,15 +14,13 @@ import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.uiautomator.UiSelector -import junit.framework.TestCase.assertTrue import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.containsString -import org.hamcrest.CoreMatchers.not -import org.hamcrest.Matchers.contains -import org.junit.Assert.assertFalse import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText +import org.mozilla.fenix.helpers.MatcherHelper.itemWithText import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime -import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.click @@ -35,10 +33,8 @@ class SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot { fun verifyNavigationToolBarHeader() = assertNavigationToolBarHeader() fun verifyTPExceptionsDefaultView() { - assertTrue( - mDevice.findObject( - UiSelector().text("Exceptions let you disable tracking protection for selected sites."), - ).waitForExists(waitingTime), + assertUIObjectExists( + itemWithText("Exceptions let you disable tracking protection for selected sites."), ) learnMoreLink.check(matches(isDisplayed())) } @@ -52,17 +48,7 @@ class SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot { fun verifySiteExceptionExists(siteUrl: String, shouldExist: Boolean) { exceptionsList.waitForExists(waitingTime) - if (shouldExist) { - assertTrue( - mDevice.findObject(UiSelector().textContains(siteUrl)) - .waitForExists(waitingTime), - ) - } else { - assertFalse( - mDevice.findObject(UiSelector().textContains(siteUrl)) - .waitForExists(waitingTimeShort), - ) - } + assertUIObjectExists(itemContainingText(siteUrl), exists = shouldExist) } class Transition { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuExperimentsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuExperimentsRobot.kt index f2f3eaef18..6326cb8f9d 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuExperimentsRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuExperimentsRobot.kt @@ -7,12 +7,10 @@ package org.mozilla.fenix.ui.robots import androidx.test.espresso.Espresso.onView import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.uiautomator.UiSelector -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId -import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.click @@ -50,17 +48,13 @@ class SettingsSubMenuExperimentsRobot { fun verifyExperimentEnrolled(title: String) { itemContainingText(title).click() - assertTrue( - checkIcon().waitForExists(waitingTimeShort), - ) + assertUIObjectExists(checkIcon()) goBackButton().click() } fun verifyExperimentNotEnrolled(title: String) { itemContainingText(title).click() - assertFalse( - checkIcon().waitForExists(waitingTimeShort), - ) + assertUIObjectExists(checkIcon(), exists = false) goBackButton().click() } @@ -68,13 +62,9 @@ class SettingsSubMenuExperimentsRobot { val branch = itemWithResId("$packageName:id/nimbus_branch_name") itemContainingText(title).click() - assertTrue( - checkIcon().waitForExists(waitingTimeShort), - ) + assertUIObjectExists(checkIcon()) branch.click() - assertFalse( - checkIcon().waitForExists(waitingTimeShort), - ) + assertUIObjectExists(checkIcon(), exists = false) } } private fun goBackButton() = onView(withContentDescription(R.string.action_bar_up_description)) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuHomepageRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuHomepageRobot.kt index c8179bc65c..df60d2d449 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuHomepageRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuHomepageRobot.kt @@ -20,8 +20,9 @@ import androidx.test.uiautomator.UiSelector import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers.allOf import org.hamcrest.Matchers -import org.junit.Assert.assertTrue import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort import org.mozilla.fenix.helpers.TestHelper import org.mozilla.fenix.helpers.TestHelper.mDevice @@ -98,11 +99,8 @@ class SettingsSubMenuHomepageRobot { mDevice.findObject(UiSelector().description(wallpaperName)).click() fun verifySnackBarText(expectedText: String) = - assertTrue( - mDevice.findObject( - UiSelector() - .textContains(expectedText), - ).waitForExists(waitingTimeShort), + assertUIObjectExists( + itemContainingText(expectedText), ) fun verifySponsoredShortcutsCheckBox(checked: Boolean) = assertSponsoredShortcutsCheckBox(checked) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuHttpsOnlyModeRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuHttpsOnlyModeRobot.kt index 48fef306ed..1894826554 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuHttpsOnlyModeRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuHttpsOnlyModeRobot.kt @@ -15,7 +15,7 @@ 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.TestHelper.getStringResource +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.assertIsChecked import org.mozilla.fenix.helpers.assertIsEnabled import org.mozilla.fenix.helpers.click diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLanguageRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLanguageRobot.kt index 3a2dfd8eac..f1feb3bd1b 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLanguageRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLanguageRobot.kt @@ -10,9 +10,10 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.uiautomator.UiScrollable import androidx.test.uiautomator.UiSelector -import junit.framework.TestCase.assertTrue import org.hamcrest.CoreMatchers import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.itemWithText import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName @@ -31,16 +32,14 @@ class SettingsSubMenuLanguageRobot { language(languageName).click() } - fun verifyLanguageHeaderIsTranslated(translation: String) = - assertTrue(mDevice.findObject(UiSelector().text(translation)).waitForExists(waitingTime)) + fun verifyLanguageHeaderIsTranslated(translation: String) = assertUIObjectExists(itemWithText(translation)) fun verifySelectedLanguage(language: String) { languagesList.waitForExists(waitingTime) - assertTrue( + assertUIObjectExists( languagesList .getChildByText(UiSelector().text(language), language, true) - .getFromParent(UiSelector().resourceId("$packageName:id/locale_selected_icon")) - .waitForExists(waitingTime), + .getFromParent(UiSelector().resourceId("$packageName:id/locale_selected_icon")), ) } @@ -53,17 +52,14 @@ class SettingsSubMenuLanguageRobot { searchBar.text = text } - fun verifySearchResultsContains(languageName: String) { - assertTrue(language(languageName).waitForExists(waitingTime)) - } + fun verifySearchResultsContains(languageName: String) = + assertUIObjectExists(language(languageName)) fun clearSearchBar() { onView(withId(R.id.search_close_btn)).click() } - fun verifyLanguageListIsDisplayed() { - assertTrue(languagesList.waitForExists(waitingTime)) - } + fun verifyLanguageListIsDisplayed() = assertUIObjectExists(languagesList) class Transition { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot.kt index 9bfc19ef53..d424ee845d 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLoginsAndPasswordOptionsToSaveRobot.kt @@ -17,8 +17,8 @@ import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.not import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText -import org.mozilla.fenix.helpers.TestHelper.getStringResource /** * Implementation of Robot Pattern for the Privacy Settings > saved logins sub menu diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.kt index 57653171eb..6afee5f399 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot.kt @@ -21,18 +21,17 @@ import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.Until import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers.containsString -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.HomeActivityIntentTestRule -import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists import org.mozilla.fenix.helpers.MatcherHelper.assertItemIsEnabledAndVisible -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.checkedItemWithResId import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText +import org.mozilla.fenix.helpers.MatcherHelper.itemWithClassNameAndIndex import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.click @@ -70,25 +69,25 @@ class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot { itemContainingText(getStringResource(R.string.preferences_logins_add_login)).click() fun verifyAddNewLoginView() { - assertItemWithResIdExists( + assertUIObjectExists( siteHeader, siteTextInput, usernameHeader, usernameTextInput, passwordHeader, passwordTextInput, + siteDescription, ) - assertItemContainingTextExists(siteDescription) siteTextInputHint.check(matches(withHint(R.string.add_login_hostname_hint_text))) } fun enterSiteCredential(website: String) = siteTextInput.setText(website) fun verifyHostnameErrorMessage() = - assertItemContainingTextExists(itemContainingText(getStringResource(R.string.add_login_hostname_invalid_text_2))) + assertUIObjectExists(itemContainingText(getStringResource(R.string.add_login_hostname_invalid_text_2))) fun verifyPasswordErrorMessage() = - assertItemContainingTextExists(itemContainingText(getStringResource(R.string.saved_login_password_required))) + assertUIObjectExists(itemContainingText(getStringResource(R.string.saved_login_password_required))) fun verifyPasswordClearButtonEnabled() = assertItemIsEnabledAndVisible(itemWithResId("$packageName:id/clearPasswordTextButton")) @@ -101,24 +100,21 @@ class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot { fun clickSavedLoginsChevronIcon() = itemWithResId("$packageName:id/toolbar_chevron_icon").click() fun verifyLoginsSortingOptions() { - assertItemContainingTextExists(itemContainingText(getStringResource(R.string.saved_logins_sort_strategy_alphabetically))) - assertItemContainingTextExists(itemContainingText(getStringResource(R.string.saved_logins_sort_strategy_last_used))) + assertUIObjectExists(itemContainingText(getStringResource(R.string.saved_logins_sort_strategy_alphabetically))) + assertUIObjectExists(itemContainingText(getStringResource(R.string.saved_logins_sort_strategy_last_used))) } fun clickLastUsedSortingOption() = itemContainingText(getStringResource(R.string.saved_logins_sort_strategy_last_used)).click() fun verifySortedLogin(position: Int, loginTitle: String) = - assertTrue( - mDevice.findObject( - UiSelector() - .className("android.view.ViewGroup") - .index(position), - ).getChild( - UiSelector() - .resourceId("$packageName:id/webAddressView") - .textContains(loginTitle), - ).waitForExists(waitingTime), + assertUIObjectExists( + itemWithClassNameAndIndex(className = "android.view.ViewGroup", index = position) + .getChild( + UiSelector() + .resourceId("$packageName:id/webAddressView") + .textContains(loginTitle), + ), ) fun searchLogin(searchTerm: String) = @@ -127,7 +123,7 @@ class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot { fun verifySavedLoginsSectionUsername(username: String) = mDevice.waitNotNull(Until.findObjects(By.text(username))) - fun verifyLoginItemUsername(username: String) = assertItemContainingTextExists(itemContainingText(username)) + fun verifyLoginItemUsername(username: String) = assertUIObjectExists(itemContainingText(username)) fun verifyNotSavedLoginFromPrompt() = onView(withText("test@example.com")) .check(ViewAssertions.doesNotExist()) @@ -145,7 +141,7 @@ class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot { fun clickDeleteLoginButton() = itemContainingText("Delete").click() fun verifyLoginDeletionPrompt() = - assertItemContainingTextExists(itemContainingText(getStringResource(R.string.login_deletion_confirmation))) + assertUIObjectExists(itemContainingText(getStringResource(R.string.login_deletion_confirmation))) fun clickConfirmDeleteLogin() = onView(withId(android.R.id.button1)).inRoot(RootMatchers.isDialog()).click() @@ -163,13 +159,11 @@ class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot { fun saveEditedLogin() = itemWithResId("$packageName:id/save_login_button").click() - fun verifySaveLoginButtonIsEnabled(isEnabled: Boolean) { - if (isEnabled) { - assertTrue(itemWithResId("$packageName:id/save_login_button").isChecked) - } else { - assertFalse(itemWithResId("$packageName:id/save_login_button").isChecked) - } - } + fun verifySaveLoginButtonIsEnabled(isEnabled: Boolean) = + assertUIObjectExists( + checkedItemWithResId("$packageName:id/save_login_button", isChecked = true), + exists = isEnabled, + ) fun revealPassword() = onView(withId(R.id.revealPasswordButton)).click() @@ -177,10 +171,10 @@ class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot { onView(withId(R.id.passwordText)).check(matches(withText(password))) fun verifyUserNameRequiredErrorMessage() = - assertItemContainingTextExists(itemContainingText(getStringResource(R.string.saved_login_username_required))) + assertUIObjectExists(itemContainingText(getStringResource(R.string.saved_login_username_required))) fun verifyPasswordRequiredErrorMessage() = - assertItemContainingTextExists(itemContainingText(getStringResource(R.string.saved_login_password_required))) + assertUIObjectExists(itemContainingText(getStringResource(R.string.saved_login_password_required))) fun clickGoBackButton() = goBackButton().click() diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuOpenLinksInAppsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuOpenLinksInAppsRobot.kt index e9a67c6f4c..e1ce8d7a45 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuOpenLinksInAppsRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuOpenLinksInAppsRobot.kt @@ -11,13 +11,11 @@ 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.MatcherHelper.assertItemContainingTextExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithDescriptionExists +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice -import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.isChecked /** @@ -26,8 +24,8 @@ import org.mozilla.fenix.helpers.isChecked class SettingsSubMenuOpenLinksInAppsRobot { fun verifyOpenLinksInAppsView(selectedOpenLinkInAppsOption: String) { - assertItemWithDescriptionExists(goBackButton) - assertItemContainingTextExists( + assertUIObjectExists( + goBackButton, itemContainingText(getStringResource(R.string.preferences_open_links_in_apps)), itemContainingText(getStringResource(R.string.preferences_open_links_in_apps_always)), itemContainingText(getStringResource(R.string.preferences_open_links_in_apps_ask)), @@ -37,8 +35,8 @@ class SettingsSubMenuOpenLinksInAppsRobot { } fun verifyPrivateOpenLinksInAppsView(selectedOpenLinkInAppsOption: String) { - assertItemWithDescriptionExists(goBackButton) - assertItemContainingTextExists( + assertUIObjectExists( + goBackButton, itemContainingText(getStringResource(R.string.preferences_open_links_in_apps)), itemContainingText(getStringResource(R.string.preferences_open_links_in_apps_ask)), itemContainingText(getStringResource(R.string.preferences_open_links_in_apps_never)), diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuPrivateBrowsingRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuPrivateBrowsingRobot.kt index 8ce3e9d9cf..2ce205706f 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuPrivateBrowsingRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuPrivateBrowsingRobot.kt @@ -19,9 +19,10 @@ import androidx.test.uiautomator.By.text import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.Until import org.hamcrest.CoreMatchers -import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.checkedItemWithResId import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestHelper.appName import org.mozilla.fenix.helpers.TestHelper.mDevice @@ -137,7 +138,10 @@ private fun assertOpenLinksInPrivateTabEnabled() = openLinksInPrivateTabSwitch().check(matches(isEnabled(true))) private fun assertOpenLinksInPrivateTabOff() { - assertFalse(mDevice.findObject(UiSelector().resourceId("android:id/switch_widget")).isChecked()) + assertUIObjectExists( + checkedItemWithResId("android:id/switch_widget", isChecked = true), + exists = false, + ) openLinksInPrivateTabSwitch() .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt index e278147eca..dd74241ac3 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt @@ -42,12 +42,14 @@ import org.hamcrest.Matchers.endsWith import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.DataGenerationHelper.getAvailableSearchEngines +import org.mozilla.fenix.helpers.DataGenerationHelper.getRegionSearchEnginesList +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText +import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText import org.mozilla.fenix.helpers.MatcherHelper.itemWithText -import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort -import org.mozilla.fenix.helpers.TestHelper.getAvailableSearchEngines -import org.mozilla.fenix.helpers.TestHelper.getRegionSearchEnginesList -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.hasCousin import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName @@ -91,9 +93,8 @@ class SettingsSubMenuSearchRobot { .check(matches(hasSibling(withText("Edit engines visible in the search menu")))) } - fun verifyEnginesShortcutsListHeader() { - assertTrue(itemWithText("Engines visible on the search menu").exists()) - } + fun verifyEnginesShortcutsListHeader() = + assertUIObjectExists(itemWithText("Engines visible on the search menu")) fun verifyAddressBarSectionHeader() { onView(withText("Address bar - Firefox Suggest")).check(matches(isDisplayed())) @@ -109,7 +110,7 @@ class SettingsSubMenuSearchRobot { defaultSearchEngineOption("DuckDuckGo") .check(matches(hasSibling(withId(R.id.engine_icon)))) .check(matches(isDisplayed())) - assertTrue(addSearchEngineButton.exists()) + assertUIObjectExists(addSearchEngineButton) } fun verifyManageShortcutsList(testRule: ComposeTestRule) { @@ -121,7 +122,7 @@ class SettingsSubMenuSearchRobot { .assertIsDisplayed() } - assertTrue(addSearchEngineButton.exists()) + assertUIObjectExists(addSearchEngineButton) } /** @@ -239,13 +240,8 @@ class SettingsSubMenuSearchRobot { fun openAddSearchEngineMenu() = addSearchEngineButton.click() - fun verifyEngineListContains(searchEngineName: String, shouldExist: Boolean) { - if (shouldExist) { - assertTrue(itemWithText(searchEngineName).waitForExists(waitingTimeShort)) - } else { - assertFalse(itemWithText(searchEngineName).waitForExists(waitingTimeShort)) - } - } + fun verifyEngineListContains(searchEngineName: String, shouldExist: Boolean) = + assertUIObjectExists(itemWithText(searchEngineName), exists = shouldExist) fun verifyDefaultSearchEngineSelected(searchEngineName: String) { defaultSearchEngineOption(searchEngineName).check(matches(isChecked(true))) @@ -264,44 +260,27 @@ class SettingsSubMenuSearchRobot { try { mDevice.findObject(By.res("$packageName:id/edit_engine_name")).clear() mDevice.findObject(By.res("$packageName:id/edit_engine_name")).text = engineName - assertTrue( - mDevice.findObject( - UiSelector() - .resourceId("$packageName:id/edit_engine_name") - .text(engineName), - ).waitForExists(waitingTime), + assertUIObjectExists( + itemWithResIdAndText("$packageName:id/edit_engine_name", engineName), ) mDevice.findObject(By.res("$packageName:id/edit_search_string")).clear() mDevice.findObject(By.res("$packageName:id/edit_search_string")).text = engineURL - assertTrue( - mDevice.findObject( - UiSelector() - .resourceId("$packageName:id/edit_search_string") - .text(engineURL), - ).waitForExists(waitingTime), + assertUIObjectExists( + itemWithResIdAndText("$packageName:id/edit_search_string", engineURL), ) } catch (e: AssertionError) { println("The name or the search string were not set properly") mDevice.findObject(By.res("$packageName:id/edit_engine_name")).clear() mDevice.findObject(By.res("$packageName:id/edit_engine_name")).setText(engineName) - assertTrue( - mDevice.findObject( - UiSelector() - .resourceId("$packageName:id/edit_engine_name") - .text(engineName), - ).waitForExists(waitingTime), + assertUIObjectExists( + itemWithResIdAndText("$packageName:id/edit_engine_name", engineName), ) - mDevice.findObject(By.res("$packageName:id/edit_search_string")).clear() mDevice.findObject(By.res("$packageName:id/edit_search_string")).setText(engineURL) - assertTrue( - mDevice.findObject( - UiSelector() - .resourceId("$packageName:id/edit_search_string") - .text(engineURL), - ).waitForExists(waitingTime), + assertUIObjectExists( + itemWithResIdAndText("$packageName:id/edit_search_string", engineURL), ) } } @@ -344,11 +323,7 @@ class SettingsSubMenuSearchRobot { fun saveEditSearchEngine() { onView(withId(R.id.save_button)).click() - assertTrue( - mDevice.findObject( - UiSelector().textContains("Saved"), - ).waitForExists(waitingTime), - ) + assertUIObjectExists(itemContainingText("Saved")) } fun verifyInvalidTemplateSearchStringFormatError() { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsCommonRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsCommonRobot.kt index 26be2beba1..922cc0beec 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsCommonRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsCommonRobot.kt @@ -1,282 +1,266 @@ -/* 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.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.isChecked -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -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 androidx.test.uiautomator.UiSelector -import org.hamcrest.CoreMatchers.allOf -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.mozilla.fenix.R -import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithDescriptionExists -import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText -import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription -import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime -import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort -import org.mozilla.fenix.helpers.TestHelper.getStringResource -import org.mozilla.fenix.helpers.TestHelper.mDevice -import org.mozilla.fenix.helpers.TestHelper.packageName -import org.mozilla.fenix.helpers.assertIsChecked -import org.mozilla.fenix.helpers.click -import org.mozilla.fenix.helpers.isChecked - -/** - * Implementation of Robot Pattern for the settings Site Permissions sub menu. - */ -class SettingsSubMenuSitePermissionsCommonRobot { - - fun verifyNavigationToolBarHeader(header: String) = assertNavigationToolBarHeader(header) - - fun verifyBlockAudioAndVideoOnMobileDataOnlyAudioAndVideoWillPlayOnWiFi() = assertBlockAudioAndVideoOnMobileDataOnlyAudioAndVideoWillPlayOnWiFi() - - fun verifyBlockAudioOnly() = assertBlockAudioOnly() - - fun verifyVideoAndAudioBlockedRecommended() = assertVideoAndAudioBlockedRecommended() - - fun verifyCheckAutoPlayRadioButtonDefault() = assertCheckAutoPayRadioButtonDefault() - - fun verifyAskToAllowButton(isChecked: Boolean = true) = - onView(withId(R.id.ask_to_allow_radio)) - .check((matches(isDisplayed()))).assertIsChecked(isChecked) - - fun verifyBlockedButton(isChecked: Boolean = false) = - onView(withId(R.id.block_radio)) - .check((matches(isDisplayed()))).assertIsChecked(isChecked) - - fun verifyBlockedByAndroid() = assertBlockedByAndroid() - - fun verifyUnblockedByAndroid() = assertUnblockedByAndroid() - - fun verifyToAllowIt() = assertToAllowIt() - - fun verifyGotoAndroidSettings() = assertGotoAndroidSettings() - - fun verifyTapPermissions() = assertTapPermissions() - - fun verifyToggleNameToON(name: String) = assertToggleNameToON(name) - - fun verifyGoToSettingsButton() = assertGoToSettingsButton() - - fun verifySitePermissionsAutoPlaySubMenuItems() { - verifyBlockAudioAndVideoOnMobileDataOnlyAudioAndVideoWillPlayOnWiFi() - verifyBlockAudioOnly() - verifyVideoAndAudioBlockedRecommended() - verifyCheckAutoPlayRadioButtonDefault() - } - - fun verifySitePermissionsCommonSubMenuItems() { - verifyAskToAllowButton() - verifyBlockedButton() - } - - fun verifyBlockedByAndroidSection() { - verifyBlockedByAndroid() - verifyToAllowIt() - verifyGotoAndroidSettings() - verifyTapPermissions() - verifyGoToSettingsButton() - } - - fun verifyNotificationSubMenuItems() { - verifyNotificationToolbar() - verifySitePermissionsCommonSubMenuItems() - } - - fun verifySitePermissionsPersistentStorageSubMenuItems() { - verifyAskToAllowButton() - verifyBlockedButton() - } - - fun verifyDRMControlledContentSubMenuItems() { - verifyAskToAllowButton() - verifyBlockedButton() - // Third option is "Allowed" - thirdRadioButton.check(matches(withText("Allowed"))) - } - - fun clickGoToSettingsButton() { - goToSettingsButton().click() - mDevice.findObject(UiSelector().resourceId("com.android.settings:id/list")) - .waitForExists(waitingTime) - } - - fun openAppSystemPermissionsSettings() { - mDevice.findObject(UiSelector().textContains("Permissions")).click() - } - - fun switchAppPermissionSystemSetting(permissionCategory: String, permission: String) { - mDevice.findObject(UiSelector().textContains(permissionCategory)).click() - - if (permission == "Allow") { - mDevice.findObject(UiSelector().textContains("Allow")).click() - } else { - mDevice.findObject(UiSelector().textContains("Deny")).click() - } - } - - fun goBackToSystemAppPermissionSettings() { - mDevice.pressBack() - mDevice.waitForIdle(waitingTime) - } - - fun goBackToPermissionsSettingsSubMenu() { - while (!permissionSettingMenu().waitForExists(waitingTimeShort)) { - mDevice.pressBack() - mDevice.waitForIdle(waitingTime) - } - } - - fun verifySystemGrantedPermission(permissionCategory: String) { - assertTrue( - mDevice.findObject( - UiSelector().className("android.widget.RelativeLayout"), - ).getChild( - UiSelector() - .resourceId("android:id/title") - .textContains(permissionCategory), - ).waitForExists(waitingTime), - ) - - assertTrue( - mDevice.findObject( - UiSelector().className("android.widget.RelativeLayout"), - ).getChild( - UiSelector() - .resourceId("android:id/summary") - .textContains("Only while app is in use"), - ).waitForExists(waitingTime), - ) - } - - fun verifyNotificationToolbar() { - assertItemContainingTextExists(itemContainingText(getStringResource(R.string.preference_phone_feature_notification))) - assertItemWithDescriptionExists(itemWithDescription(getStringResource(R.string.action_bar_up_description))) - } - - fun selectAutoplayOption(text: String) { - when (text) { - "Allow audio and video" -> askToAllowRadioButton.click() - "Block audio and video on cellular data only" -> blockRadioButton.click() - "Block audio only" -> thirdRadioButton.click() - "Block audio and video" -> fourthRadioButton.click() - } - } - - fun selectPermissionSettingOption(text: String) { - when (text) { - "Ask to allow" -> askToAllowRadioButton.click() - "Blocked" -> blockRadioButton.click() - } - } - - fun selectDRMControlledContentPermissionSettingOption(text: String) { - when (text) { - "Ask to allow" -> askToAllowRadioButton.click() - "Blocked" -> blockRadioButton.click() - "Allowed" -> thirdRadioButton.click() - } - } - - class Transition { - fun goBack(interact: SettingsSubMenuSitePermissionsRobot.() -> Unit): SettingsSubMenuSitePermissionsRobot.Transition { - goBackButton().click() - - SettingsSubMenuSitePermissionsRobot().interact() - return SettingsSubMenuSitePermissionsRobot.Transition() - } - } -} - -// common Blocked radio button for all settings -private val blockRadioButton = onView(withId(R.id.block_radio)) - -// common Ask to Allow radio button for all settings -private val askToAllowRadioButton = onView(withId(R.id.ask_to_allow_radio)) - -// common extra 3rd radio button for all settings -private val thirdRadioButton = onView(withId(R.id.third_radio)) - -// common extra 4th radio button for all settings -private val fourthRadioButton = onView(withId(R.id.fourth_radio)) - -private fun assertNavigationToolBarHeader(header: String) = onView(allOf(withContentDescription(header))) - -private fun assertBlockAudioAndVideoOnMobileDataOnlyAudioAndVideoWillPlayOnWiFi() = - blockRadioButton.check((matches(withEffectiveVisibility(Visibility.VISIBLE)))) - -private fun assertBlockAudioOnly() = - thirdRadioButton.check((matches(withEffectiveVisibility(Visibility.VISIBLE)))) - -private fun assertVideoAndAudioBlockedRecommended() = onView(withId(R.id.fourth_radio)) - .check((matches(withEffectiveVisibility(Visibility.VISIBLE)))) - -private fun assertCheckAutoPayRadioButtonDefault() { - // Allow audio and video - askToAllowRadioButton - .assertIsChecked(isChecked = false) - - // Block audio and video on cellular data only - blockRadioButton - .assertIsChecked(isChecked = false) - - // Block audio only (default) - thirdRadioButton - .assertIsChecked(isChecked = true) - - // Block audio and video - fourthRadioButton - .assertIsChecked(isChecked = false) -} - -private fun assertBlockedByAndroid() { - blockedByAndroidContainer().waitForExists(waitingTime) - assertTrue( - mDevice.findObject( - UiSelector().textContains(getStringResource(R.string.phone_feature_blocked_by_android)), - ).waitForExists(waitingTimeShort), - ) -} - -private fun assertUnblockedByAndroid() { - blockedByAndroidContainer().waitUntilGone(waitingTime) - assertFalse( - mDevice.findObject( - UiSelector().textContains(getStringResource(R.string.phone_feature_blocked_by_android)), - ).waitForExists(waitingTimeShort), - ) -} - -private fun blockedByAndroidContainer() = mDevice.findObject(UiSelector().resourceId("$packageName:id/permissions_blocked_container")) - -private fun permissionSettingMenu() = mDevice.findObject(UiSelector().resourceId("$packageName:id/container")) - -private fun assertToAllowIt() = onView(withText(R.string.phone_feature_blocked_intro)) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - -private fun assertGotoAndroidSettings() = onView(withText(R.string.phone_feature_blocked_step_settings)) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - -private fun assertTapPermissions() = onView(withText("2. Tap Permissions")) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - -private fun assertToggleNameToON(name: String) = onView(withText(name)) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - -private fun assertGoToSettingsButton() = - goToSettingsButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - -private fun goBackButton() = - onView(allOf(withContentDescription("Navigate up"))) - -private fun goToSettingsButton() = onView(withId(R.id.settings_button)) +/* 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.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.isDisplayed +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 androidx.test.uiautomator.UiSelector +import org.hamcrest.CoreMatchers.allOf +import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText +import org.mozilla.fenix.helpers.MatcherHelper.itemWithClassName +import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription +import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime +import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort +import org.mozilla.fenix.helpers.TestHelper.mDevice +import org.mozilla.fenix.helpers.TestHelper.packageName +import org.mozilla.fenix.helpers.assertIsChecked +import org.mozilla.fenix.helpers.click + +/** + * Implementation of Robot Pattern for the settings Site Permissions sub menu. + */ +class SettingsSubMenuSitePermissionsCommonRobot { + + fun verifyNavigationToolBarHeader(header: String) = assertNavigationToolBarHeader(header) + + fun verifyBlockAudioAndVideoOnMobileDataOnlyAudioAndVideoWillPlayOnWiFi() = assertBlockAudioAndVideoOnMobileDataOnlyAudioAndVideoWillPlayOnWiFi() + + fun verifyBlockAudioOnly() = assertBlockAudioOnly() + + fun verifyVideoAndAudioBlockedRecommended() = assertVideoAndAudioBlockedRecommended() + + fun verifyCheckAutoPlayRadioButtonDefault() = assertCheckAutoPayRadioButtonDefault() + + fun verifyAskToAllowButton(isChecked: Boolean = true) = + onView(withId(R.id.ask_to_allow_radio)) + .check((matches(isDisplayed()))).assertIsChecked(isChecked) + + fun verifyBlockedButton(isChecked: Boolean = false) = + onView(withId(R.id.block_radio)) + .check((matches(isDisplayed()))).assertIsChecked(isChecked) + + fun verifyBlockedByAndroid() = assertBlockedByAndroid() + + fun verifyUnblockedByAndroid() = assertUnblockedByAndroid() + + fun verifyToAllowIt() = assertToAllowIt() + + fun verifyGotoAndroidSettings() = assertGotoAndroidSettings() + + fun verifyTapPermissions() = assertTapPermissions() + + fun verifyToggleNameToON(name: String) = assertToggleNameToON(name) + + fun verifyGoToSettingsButton() = assertGoToSettingsButton() + + fun verifySitePermissionsAutoPlaySubMenuItems() { + verifyBlockAudioAndVideoOnMobileDataOnlyAudioAndVideoWillPlayOnWiFi() + verifyBlockAudioOnly() + verifyVideoAndAudioBlockedRecommended() + verifyCheckAutoPlayRadioButtonDefault() + } + + fun verifySitePermissionsCommonSubMenuItems() { + verifyAskToAllowButton() + verifyBlockedButton() + } + + fun verifyBlockedByAndroidSection() { + verifyBlockedByAndroid() + verifyToAllowIt() + verifyGotoAndroidSettings() + verifyTapPermissions() + verifyGoToSettingsButton() + } + + fun verifyNotificationSubMenuItems() { + verifyNotificationToolbar() + verifySitePermissionsCommonSubMenuItems() + } + + fun verifySitePermissionsPersistentStorageSubMenuItems() { + verifyAskToAllowButton() + verifyBlockedButton() + } + + fun verifyDRMControlledContentSubMenuItems() { + verifyAskToAllowButton() + verifyBlockedButton() + // Third option is "Allowed" + thirdRadioButton.check(matches(withText("Allowed"))) + } + + fun clickGoToSettingsButton() { + goToSettingsButton().click() + mDevice.findObject(UiSelector().resourceId("com.android.settings:id/list")) + .waitForExists(waitingTime) + } + + fun openAppSystemPermissionsSettings() { + mDevice.findObject(UiSelector().textContains("Permissions")).click() + } + + fun switchAppPermissionSystemSetting(permissionCategory: String, permission: String) { + mDevice.findObject(UiSelector().textContains(permissionCategory)).click() + + if (permission == "Allow") { + mDevice.findObject(UiSelector().textContains("Allow")).click() + } else { + mDevice.findObject(UiSelector().textContains("Deny")).click() + } + } + + fun goBackToSystemAppPermissionSettings() { + mDevice.pressBack() + mDevice.waitForIdle(waitingTime) + } + + fun goBackToPermissionsSettingsSubMenu() { + while (!permissionSettingMenu().waitForExists(waitingTimeShort)) { + mDevice.pressBack() + mDevice.waitForIdle(waitingTime) + } + } + + fun verifySystemGrantedPermission(permissionCategory: String) { + assertUIObjectExists( + itemWithClassName("android.widget.RelativeLayout") + .getChild( + UiSelector() + .resourceId("android:id/title") + .textContains(permissionCategory), + ), + itemWithClassName("android.widget.RelativeLayout") + .getChild( + UiSelector() + .resourceId("android:id/summary") + .textContains("Only while app is in use"), + ), + ) + } + + fun verifyNotificationToolbar() = + assertUIObjectExists( + itemContainingText(getStringResource(R.string.preference_phone_feature_notification)), + itemWithDescription(getStringResource(R.string.action_bar_up_description)), + ) + + fun selectAutoplayOption(text: String) { + when (text) { + "Allow audio and video" -> askToAllowRadioButton.click() + "Block audio and video on cellular data only" -> blockRadioButton.click() + "Block audio only" -> thirdRadioButton.click() + "Block audio and video" -> fourthRadioButton.click() + } + } + + fun selectPermissionSettingOption(text: String) { + when (text) { + "Ask to allow" -> askToAllowRadioButton.click() + "Blocked" -> blockRadioButton.click() + } + } + + fun selectDRMControlledContentPermissionSettingOption(text: String) { + when (text) { + "Ask to allow" -> askToAllowRadioButton.click() + "Blocked" -> blockRadioButton.click() + "Allowed" -> thirdRadioButton.click() + } + } + + class Transition { + fun goBack(interact: SettingsSubMenuSitePermissionsRobot.() -> Unit): SettingsSubMenuSitePermissionsRobot.Transition { + goBackButton().click() + + SettingsSubMenuSitePermissionsRobot().interact() + return SettingsSubMenuSitePermissionsRobot.Transition() + } + } +} + +// common Blocked radio button for all settings +private val blockRadioButton = onView(withId(R.id.block_radio)) + +// common Ask to Allow radio button for all settings +private val askToAllowRadioButton = onView(withId(R.id.ask_to_allow_radio)) + +// common extra 3rd radio button for all settings +private val thirdRadioButton = onView(withId(R.id.third_radio)) + +// common extra 4th radio button for all settings +private val fourthRadioButton = onView(withId(R.id.fourth_radio)) + +private fun assertNavigationToolBarHeader(header: String) = onView(allOf(withContentDescription(header))) + +private fun assertBlockAudioAndVideoOnMobileDataOnlyAudioAndVideoWillPlayOnWiFi() = + blockRadioButton.check((matches(withEffectiveVisibility(Visibility.VISIBLE)))) + +private fun assertBlockAudioOnly() = + thirdRadioButton.check((matches(withEffectiveVisibility(Visibility.VISIBLE)))) + +private fun assertVideoAndAudioBlockedRecommended() = onView(withId(R.id.fourth_radio)) + .check((matches(withEffectiveVisibility(Visibility.VISIBLE)))) + +private fun assertCheckAutoPayRadioButtonDefault() { + // Allow audio and video + askToAllowRadioButton + .assertIsChecked(isChecked = false) + + // Block audio and video on cellular data only + blockRadioButton + .assertIsChecked(isChecked = false) + + // Block audio only (default) + thirdRadioButton + .assertIsChecked(isChecked = true) + + // Block audio and video + fourthRadioButton + .assertIsChecked(isChecked = false) +} + +private fun assertBlockedByAndroid() { + blockedByAndroidContainer().waitForExists(waitingTime) + assertUIObjectExists(itemContainingText(getStringResource(R.string.phone_feature_blocked_by_android))) +} + +private fun assertUnblockedByAndroid() { + blockedByAndroidContainer().waitUntilGone(waitingTime) + assertUIObjectExists(itemContainingText(getStringResource(R.string.phone_feature_blocked_by_android)), exists = false) +} + +private fun blockedByAndroidContainer() = mDevice.findObject(UiSelector().resourceId("$packageName:id/permissions_blocked_container")) + +private fun permissionSettingMenu() = mDevice.findObject(UiSelector().resourceId("$packageName:id/container")) + +private fun assertToAllowIt() = onView(withText(R.string.phone_feature_blocked_intro)) + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + +private fun assertGotoAndroidSettings() = onView(withText(R.string.phone_feature_blocked_step_settings)) + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + +private fun assertTapPermissions() = onView(withText("2. Tap Permissions")) + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + +private fun assertToggleNameToON(name: String) = onView(withText(name)) + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + +private fun assertGoToSettingsButton() = + goToSettingsButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + +private fun goBackButton() = + onView(allOf(withContentDescription("Navigate up"))) + +private fun goToSettingsButton() = onView(withId(R.id.settings_button)) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsExceptionsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsExceptionsRobot.kt index 8f0acda55d..a95a24a208 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsExceptionsRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsExceptionsRobot.kt @@ -14,10 +14,11 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.uiautomator.UiSelector import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.containsString -import org.junit.Assert.assertTrue import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectIsGone +import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.click @@ -37,9 +38,7 @@ class SettingsSubMenuSitePermissionsExceptionsRobot { exceptionsList.waitForExists(waitingTime) onView(withText(containsString(url))).check(matches(isDisplayed())) } else { - assertTrue( - mDevice.findObject(UiSelector().textContains(url)).waitUntilGone(waitingTime), - ) + assertUIObjectIsGone(itemContainingText(url)) } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ShareOverlayRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ShareOverlayRobot.kt index 3bcc22d342..951930a245 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ShareOverlayRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ShareOverlayRobot.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.ui.robots import android.content.Intent import android.net.Uri +import android.util.Log import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.intent.Intents @@ -20,13 +21,14 @@ 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.MatcherHelper.assertItemContainingTextExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndTextExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists +import org.mozilla.fenix.helpers.Constants.TAG +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText -import org.mozilla.fenix.helpers.TestHelper.getStringResource +import org.mozilla.fenix.helpers.MatcherHelper.itemWithText +import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.ext.waitNotNull @@ -52,7 +54,7 @@ class ShareOverlayRobot { // This function verifies the share layout when a single tab is shared - no tab info shown fun verifyShareTabLayout() { - assertItemWithResIdExists( + assertUIObjectExists( // Share layout itemWithResId("$packageName:id/sharingLayout"), // Send to device section @@ -61,8 +63,6 @@ class ShareOverlayRobot { itemWithResId("$packageName:id/recentAppsContainer"), // All actions sections itemWithResId("$packageName:id/appsList"), - ) - assertItemWithResIdAndTextExists( // Send to device header itemWithResIdContainingText( "$packageName:id/accountHeaderText", @@ -78,9 +78,6 @@ class ShareOverlayRobot { "$packageName:id/apps_link_header", getStringResource(R.string.share_link_all_apps_subheader), ), - ) - - assertItemContainingTextExists( // Save as PDF button itemContainingText(getStringResource(R.string.share_save_to_pdf)), ) @@ -135,10 +132,19 @@ class ShareOverlayRobot { class Transition { fun clickSaveAsPDF(interact: DownloadRobot.() -> Unit): DownloadRobot.Transition { itemContainingText("Save as PDF").click() + Log.i(TAG, "clickSaveAsPDF: Clicked \"SAVE AS PDF\" share overlay button") DownloadRobot().interact() return DownloadRobot.Transition() } + + fun clickPrintButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + itemWithText("Print").waitForExists(TestAssetHelper.waitingTime) + itemWithText("Print").click() + + BrowserRobot().interact() + return BrowserRobot.Transition() + } } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SitePermissionsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SitePermissionsRobot.kt index bcc9590636..54a347fbfb 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SitePermissionsRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SitePermissionsRobot.kt @@ -10,9 +10,10 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.uiautomator.UiSelector -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.MatcherHelper.assertItemTextEquals +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.itemWithText import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName @@ -21,78 +22,57 @@ import org.mozilla.fenix.helpers.click class SitePermissionsRobot { fun verifyMicrophonePermissionPrompt(url: String) { try { - assertTrue( - mDevice.findObject(UiSelector().text("Allow $url to use your microphone?")) - .waitForExists(waitingTime), - ) - assertTrue(denyPagePermissionButton.text.equals("Don’t allow")) - assertTrue(allowPagePermissionButton.text.equals("Allow")) + assertUIObjectExists(itemWithText("Allow $url to use your microphone?")) + assertItemTextEquals(denyPagePermissionButton, expectedText = "Don’t allow") + assertItemTextEquals(allowPagePermissionButton, expectedText = "Allow") } catch (e: AssertionError) { browserScreen { }.openThreeDotMenu { }.refreshPage { }.clickStartMicrophoneButton { - assertTrue( - mDevice.findObject(UiSelector().text("Allow $url to use your microphone?")) - .waitForExists(waitingTime), - ) - assertTrue(denyPagePermissionButton.text.equals("Don’t allow")) - assertTrue(allowPagePermissionButton.text.equals("Allow")) + assertUIObjectExists(itemWithText("Allow $url to use your microphone?")) + assertItemTextEquals(denyPagePermissionButton, expectedText = "Don’t allow") + assertItemTextEquals(allowPagePermissionButton, expectedText = "Allow") } } } fun verifyCameraPermissionPrompt(url: String) { try { - assertTrue( - mDevice.findObject(UiSelector().text("Allow $url to use your camera?")) - .waitForExists(waitingTime), - ) - assertTrue(denyPagePermissionButton.text.equals("Don’t allow")) - assertTrue(allowPagePermissionButton.text.equals("Allow")) + assertUIObjectExists(itemWithText("Allow $url to use your camera?")) + assertItemTextEquals(denyPagePermissionButton, expectedText = "Don’t allow") + assertItemTextEquals(allowPagePermissionButton, expectedText = "Allow") } catch (e: AssertionError) { browserScreen { }.openThreeDotMenu { }.refreshPage { }.clickStartCameraButton { - assertTrue( - mDevice.findObject(UiSelector().text("Allow $url to use your camera?")) - .waitForExists(waitingTime), - ) - assertTrue(denyPagePermissionButton.text.equals("Don’t allow")) - assertTrue(allowPagePermissionButton.text.equals("Allow")) + assertUIObjectExists(itemWithText("Allow $url to use your camera?")) + assertItemTextEquals(denyPagePermissionButton, expectedText = "Don’t allow") + assertItemTextEquals(allowPagePermissionButton, expectedText = "Allow") } } } fun verifyAudioVideoPermissionPrompt(url: String) { - assertTrue( - mDevice.findObject(UiSelector().text("Allow $url to use your camera and microphone?")) - .waitForExists(waitingTime), - ) - assertTrue(denyPagePermissionButton.text.equals("Don’t allow")) - assertTrue(allowPagePermissionButton.text.equals("Allow")) + assertUIObjectExists(itemWithText("Allow $url to use your camera and microphone?")) + assertItemTextEquals(denyPagePermissionButton, expectedText = "Don’t allow") + assertItemTextEquals(allowPagePermissionButton, expectedText = "Allow") } fun verifyLocationPermissionPrompt(url: String) { try { - assertTrue( - mDevice.findObject(UiSelector().text("Allow $url to use your location?")) - .waitForExists(waitingTime), - ) - assertTrue(denyPagePermissionButton.text.equals("Don’t allow")) - assertTrue(allowPagePermissionButton.text.equals("Allow")) + assertUIObjectExists(itemWithText("Allow $url to use your location?")) + assertItemTextEquals(denyPagePermissionButton, expectedText = "Don’t allow") + assertItemTextEquals(allowPagePermissionButton, expectedText = "Allow") } catch (e: AssertionError) { browserScreen { }.openThreeDotMenu { }.refreshPage { }.clickGetLocationButton { - assertTrue( - mDevice.findObject(UiSelector().text("Allow $url to use your location?")) - .waitForExists(waitingTime), - ) - assertTrue(denyPagePermissionButton.text.equals("Don’t allow")) - assertTrue(allowPagePermissionButton.text.equals("Allow")) + assertUIObjectExists(itemWithText("Allow $url to use your location?")) + assertItemTextEquals(denyPagePermissionButton, expectedText = "Don’t allow") + assertItemTextEquals(allowPagePermissionButton, expectedText = "Allow") } } } @@ -100,77 +80,56 @@ class SitePermissionsRobot { fun verifyNotificationsPermissionPrompt(url: String, blocked: Boolean = false) { if (!blocked) { try { - assertTrue( - mDevice.findObject(UiSelector().text("Allow $url to send notifications?")) - .waitForExists(waitingTime), - ) - assertTrue(denyPagePermissionButton.text.equals("Never")) - assertTrue(allowPagePermissionButton.text.equals("Always")) + assertUIObjectExists(itemWithText("Allow $url to send notifications?")) + assertItemTextEquals(denyPagePermissionButton, expectedText = "Never") + assertItemTextEquals(allowPagePermissionButton, expectedText = "Always") } catch (e: AssertionError) { browserScreen { }.openThreeDotMenu { }.refreshPage { }.clickOpenNotificationButton { - assertTrue( - mDevice.findObject(UiSelector().text("Allow $url to send notifications?")) - .waitForExists(waitingTime), - ) - assertTrue(denyPagePermissionButton.text.equals("Never")) - assertTrue(allowPagePermissionButton.text.equals("Always")) + assertUIObjectExists(itemWithText("Allow $url to send notifications?")) + assertItemTextEquals(denyPagePermissionButton, expectedText = "Never") + assertItemTextEquals(allowPagePermissionButton, expectedText = "Always") } } } else { /* if "Never" was selected in a previous step, or if the app is not allowed, the Notifications permission prompt won't be displayed anymore */ - assertFalse( - mDevice.findObject(UiSelector().text("Allow $url to send notifications?")) - .exists(), - ) + assertUIObjectExists(itemWithText("Allow $url to send notifications?"), exists = false) } } fun verifyPersistentStoragePermissionPrompt(url: String) { try { - assertTrue( - mDevice.findObject(UiSelector().text("Allow $url to store data in persistent storage?")) - .waitForExists(waitingTime), - ) - assertTrue(denyPagePermissionButton.text.equals("Don’t allow")) - assertTrue(allowPagePermissionButton.text.equals("Allow")) + assertUIObjectExists(itemWithText("Allow $url to store data in persistent storage?")) + assertItemTextEquals(denyPagePermissionButton, expectedText = "Don’t allow") + assertItemTextEquals(allowPagePermissionButton, expectedText = "Allow") } catch (e: AssertionError) { browserScreen { }.openThreeDotMenu { }.refreshPage { }.clickRequestPersistentStorageAccessButton { - assertTrue( - mDevice.findObject(UiSelector().text("Allow $url to store data in persistent storage?")) - .waitForExists(waitingTime), - ) - assertTrue(denyPagePermissionButton.text.equals("Don’t allow")) - assertTrue(allowPagePermissionButton.text.equals("Allow")) + assertUIObjectExists(itemWithText("Allow $url to store data in persistent storage?")) + assertItemTextEquals(denyPagePermissionButton, expectedText = "Don’t allow") + assertItemTextEquals(allowPagePermissionButton, expectedText = "Allow") } } } fun verifyDRMContentPermissionPrompt(url: String) { try { - assertTrue( - mDevice.findObject(UiSelector().text("Allow $url to play DRM-controlled content?")) - .waitForExists(waitingTime), - ) - assertTrue(denyPagePermissionButton.text.equals("Don’t allow")) - assertTrue(allowPagePermissionButton.text.equals("Allow")) + assertUIObjectExists(itemWithText("Allow $url to store data in persistent storage?")) + assertItemTextEquals(denyPagePermissionButton, expectedText = "Don’t allow") + assertItemTextEquals(allowPagePermissionButton, expectedText = "Allow") } catch (e: AssertionError) { browserScreen { }.openThreeDotMenu { }.refreshPage { }.clickRequestDRMControlledContentAccessButton { - assertTrue( - mDevice.findObject(UiSelector().text("Allow $url to play DRM-controlled content?")) - .waitForExists(waitingTime), - ) - assertTrue(denyPagePermissionButton.text.equals("Don’t allow")) - assertTrue(allowPagePermissionButton.text.equals("Allow")) + assertUIObjectExists(itemWithText("Allow $url to store data in persistent storage?")) + assertItemTextEquals(denyPagePermissionButton, expectedText = "Don’t allow") + assertItemTextEquals(allowPagePermissionButton, expectedText = "Allow") } } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SiteSecurityRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SiteSecurityRobot.kt index b5ce4345a5..2995189ce9 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SiteSecurityRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SiteSecurityRobot.kt @@ -13,11 +13,11 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.uiautomator.UiSelector import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl -import org.junit.Assert.assertTrue import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName @@ -41,23 +41,27 @@ class SiteSecurityRobot { private fun assertQuickActionSheet(url: String = "", isConnectionSecure: Boolean) { quickActionSheet().waitForExists(waitingTime) - assertTrue(quickActionSheetUrl(url.tryGetHostFromUrl()).waitForExists(waitingTime)) - assertTrue(quickActionSheetSecurityInfo(isConnectionSecure).waitForExists(waitingTime)) - assertTrue(quickActionSheetTrackingProtectionSwitch().waitForExists(waitingTime)) - assertTrue(quickActionSheetClearSiteData().waitForExists(waitingTime)) + assertUIObjectExists( + quickActionSheetUrl(url.tryGetHostFromUrl()), + quickActionSheetSecurityInfo(isConnectionSecure), + quickActionSheetTrackingProtectionSwitch(), + quickActionSheetClearSiteData(), + ) } private fun assertSecureConnectionSubMenu(pageTitle: String = "", url: String = "", isConnectionSecure: Boolean) { secureConnectionSubMenu().waitForExists(waitingTime) - assertTrue(secureConnectionSubMenuPageTitle(pageTitle).waitForExists(waitingTime)) - assertTrue(secureConnectionSubMenuPageUrl(url).waitForExists(waitingTime)) - assertTrue(secureConnectionSubMenuLockIcon().waitForExists(waitingTime)) - assertTrue(secureConnectionSubMenuSecurityInfo(isConnectionSecure).waitForExists(waitingTime)) - assertTrue(secureConnectionSubMenuCertificateInfo().waitForExists(waitingTime)) + assertUIObjectExists( + secureConnectionSubMenuPageTitle(pageTitle), + secureConnectionSubMenuPageUrl(url), + secureConnectionSubMenuSecurityInfo(isConnectionSecure), + secureConnectionSubMenuLockIcon(), + secureConnectionSubMenuCertificateInfo(), + ) } private fun assertClearSiteDataPrompt(url: String) { - assertTrue(clearSiteDataPrompt(url).waitForExists(waitingTime)) + assertUIObjectExists(clearSiteDataPrompt(url)) cancelClearSiteDataButton.check(matches(isDisplayed())) deleteSiteDataButton.check(matches(isDisplayed())) } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SyncSignInRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SyncSignInRobot.kt index 9392ccffe7..4d9d5975c2 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SyncSignInRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SyncSignInRobot.kt @@ -12,8 +12,9 @@ import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.uiautomator.UiSelector import org.hamcrest.CoreMatchers.allOf -import org.junit.Assert.assertTrue import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName @@ -27,12 +28,9 @@ class SyncSignInRobot { fun verifyAccountSettingsMenuHeader() = assertAccountSettingsMenuHeader() fun verifyTurnOnSyncMenu() { mDevice.findObject(UiSelector().resourceId("$packageName:id/container")).waitForExists(waitingTime) - assertTrue( - mDevice.findObject( - UiSelector() - .resourceId("$packageName:id/signInScanButton") - .resourceId("$packageName:id/signInEmailButton"), - ).waitForExists(waitingTime), + assertUIObjectExists( + itemWithResId("$packageName:id/signInScanButton"), + itemWithResId("$packageName:id/signInEmailButton"), ) } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SystemSettingsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SystemSettingsRobot.kt index 7df5a8994c..17fdc483d1 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SystemSettingsRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SystemSettingsRobot.kt @@ -9,6 +9,8 @@ import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction import androidx.test.uiautomator.UiSelector import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndDescription import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestHelper @@ -63,10 +65,7 @@ fun systemSettings(interact: SystemSettingsRobot.() -> Unit): SystemSettingsRobo private fun assertSystemNotificationsView() { mDevice.findObject(UiSelector().resourceId("com.android.settings:id/list")) .waitForExists(waitingTime) - assertTrue( - mDevice.findObject(UiSelector().textContains("All ${TestHelper.appName} notifications")) - .waitForExists(waitingTime), - ) + assertUIObjectExists(itemContainingText("All ${TestHelper.appName} notifications")) } private val allSystemSettingsNotificationsToggle = diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/TabDrawerRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/TabDrawerRobot.kt index 277f58e7e3..c7b833dc85 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/TabDrawerRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/TabDrawerRobot.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.ui.robots +import android.util.Log import android.view.View import androidx.test.espresso.Espresso.onView import androidx.test.espresso.UiController @@ -30,8 +31,6 @@ import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until.findObject import com.google.android.material.bottomsheet.BottomSheetBehavior -import junit.framework.TestCase.assertFalse -import junit.framework.TestCase.assertTrue import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.CoreMatchers.containsString @@ -39,17 +38,17 @@ import org.hamcrest.Matcher import org.mozilla.fenix.R import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION import org.mozilla.fenix.helpers.Constants.RETRY_COUNT -import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndTextExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectIsGone import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText +import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndDescription import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeLong import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText @@ -123,7 +122,7 @@ class TabDrawerRobot { fun verifyHalfExpandedRatio() = assertMinisculeHalfExpandedRatio() fun verifyBehaviorState(expectedState: Int) = assertBehaviorState(expectedState) fun verifyOpenedTabThumbnail() = - assertItemWithResIdExists(itemWithResId("$packageName:id/mozac_browser_tabstray_thumbnail")) + assertUIObjectExists(itemWithResId("$packageName:id/mozac_browser_tabstray_thumbnail")) fun closeTab() { closeTabButton().waitForExists(waitingTime) @@ -131,6 +130,7 @@ class TabDrawerRobot { var retries = 0 // number of retries before failing, will stop at 2 do { closeTabButton().click() + Log.i("MozTestLog", "Clicked the close tab button in tabs tray. Retry #$retries") retries++ } while (closeTabButton().exists() && retries < 3) } @@ -158,11 +158,12 @@ class TabDrawerRobot { ), ), ).perform(swipeRight()) - assertTrue( + Log.i("MozTestLog", "Tab $title swiped right from tabs tray. Retry # $i") + assertUIObjectIsGone( itemWithResIdContainingText( "$packageName:id/mozac_browser_tabstray_title", title, - ).waitUntilGone(waitingTimeShort), + ), ) break @@ -188,11 +189,12 @@ class TabDrawerRobot { ), ), ).perform(swipeLeft()) - assertTrue( + Log.i("MozTestLog", "Tab $title swiped left from tabs tray. Retry # $i") + assertUIObjectIsGone( itemWithResIdContainingText( "$packageName:id/mozac_browser_tabstray_title", title, - ).waitUntilGone(waitingTimeShort), + ), ) break @@ -204,13 +206,8 @@ class TabDrawerRobot { } } - fun verifySnackBarText(expectedText: String) { - assertTrue( - mDevice.findObject( - UiSelector().text(expectedText), - ).waitForExists(waitingTime), - ) - } + fun verifySnackBarText(expectedText: String) = + assertUIObjectExists(itemContainingText(expectedText)) fun snackBarButtonClick(expectedText: String) { val snackBarButton = @@ -224,8 +221,7 @@ class TabDrawerRobot { snackBarButton.click() } - fun verifyTabMediaControlButtonState(action: String) = - assertTrue(tabMediaControlButton(action).waitForExists(waitingTime)) + fun verifyTabMediaControlButtonState(action: String) = assertUIObjectExists(tabMediaControlButton(action)) fun clickTabMediaControlButton(action: String) { tabMediaControlButton(action).also { @@ -285,18 +281,17 @@ class TabDrawerRobot { } fun verifyTabsMultiSelectionCounter(numOfTabs: Int) = - assertItemWithResIdAndTextExists( + assertUIObjectExists( itemWithResId("$packageName:id/multiselect_title"), itemContainingText("$numOfTabs selected"), ) - fun verifySyncedTabsListWhenUserIsNotSignedIn() { - assertItemWithResIdExists(itemWithResId("$packageName:id/tabsTray")) - assertItemContainingTextExists( + fun verifySyncedTabsListWhenUserIsNotSignedIn() = + assertUIObjectExists( + itemWithResId("$packageName:id/tabsTray"), itemContainingText(getStringResource(R.string.synced_tabs_sign_in_message)), itemContainingText(getStringResource(R.string.sync_sign_in)), ) - } class Transition { fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): Transition { @@ -483,12 +478,8 @@ private fun closeTabButton() = mDevice.findObject(UiSelector().descriptionContains("Close tab")) private fun assertCloseTabsButton(title: String) = - assertTrue( - mDevice.findObject( - UiSelector() - .descriptionContains("Close tab"), - ).getFromParent(UiSelector().textContains(title)) - .waitForExists(waitingTime), + assertUIObjectExists( + itemWithDescription("Close tab").getFromParent(UiSelector().textContains(title)), ) private fun normalBrowsingButton() = onView( @@ -512,18 +503,14 @@ private fun assertExistingOpenTabs(vararg tabTitles: String) { while (!tabItem(title).waitForExists(waitingTime) && retries++ < 3) { tabsList .getChildByText(UiSelector().text(title), title, true) - assertTrue( - tabItem(title).waitForExists(waitingTimeLong), - ) + assertUIObjectExists(tabItem(title), waitingTime = waitingTimeLong) } } } private fun assertNoExistingOpenTabs(vararg tabTitles: String) { for (title in tabTitles) { - assertFalse( - tabItem(title).waitForExists(waitingTimeShort), - ) + assertUIObjectExists(tabItem(title), exists = false) } } @@ -531,12 +518,7 @@ private fun assertExistingTabList() { mDevice.findObject( UiSelector().resourceId("$packageName:id/tabsTray"), ).waitForExists(waitingTime) - - assertTrue( - mDevice.findObject( - UiSelector().resourceId("$packageName:id/tray_list_item"), - ).waitForExists(waitingTime), - ) + assertUIObjectExists(itemWithResId("$packageName:id/tray_list_item")) } private fun assertNoOpenTabsInNormalBrowsing() = diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt index d29a640ebe..3a9b436910 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.ui.robots +import android.util.Log import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.swipeDown @@ -25,23 +26,21 @@ import androidx.test.uiautomator.UiObjectNotFoundException import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.Until import org.hamcrest.Matchers.allOf -import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.mozilla.fenix.R import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION import org.mozilla.fenix.helpers.Constants.RETRY_COUNT -import org.mozilla.fenix.helpers.MatcherHelper.assertCheckedItemWithResIdAndTextExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithDescriptionExists -import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndTextExists +import org.mozilla.fenix.helpers.Constants.TAG +import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource +import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists import org.mozilla.fenix.helpers.MatcherHelper.checkedItemWithResIdAndText import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription +import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText import org.mozilla.fenix.helpers.MatcherHelper.itemWithText import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeLong -import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.click @@ -54,10 +53,10 @@ import org.mozilla.fenix.nimbus.FxNimbus @Suppress("ForbiddenComment") class ThreeDotMenuMainRobot { fun verifyShareAllTabsButton() = assertShareAllTabsButton() - fun verifySettingsButton() = assertItemContainingTextExists(settingsButton()) - fun verifyHistoryButton() = assertItemContainingTextExists(historyButton) + fun verifySettingsButton() = assertUIObjectExists(settingsButton()) + fun verifyHistoryButton() = assertUIObjectExists(historyButton) fun verifyThreeDotMenuExists() = threeDotMenuRecyclerViewExists() - fun verifyAddBookmarkButton() = assertItemWithResIdAndTextExists(addBookmarkButton) + fun verifyAddBookmarkButton() = assertUIObjectExists(addBookmarkButton) fun verifyEditBookmarkButton() = assertEditBookmarkButton() fun verifyCloseAllTabsButton() = assertCloseAllTabsButton() fun verifyReaderViewAppearance(visible: Boolean) = assertReaderViewAppearanceButton(visible) @@ -67,7 +66,7 @@ class ThreeDotMenuMainRobot { // In case it reaches the end, the second swipe is no-op. expandMenu() expandMenu() - assertItemContainingTextExists(itemWithText("Quit")) + assertUIObjectExists(itemWithText("Quit")) } fun expandMenu() { @@ -77,20 +76,20 @@ class ThreeDotMenuMainRobot { fun verifyShareTabButton() = assertShareTabButton() fun verifySelectTabs() = assertSelectTabsButton() - fun verifyFindInPageButton() = assertItemContainingTextExists(findInPageButton) + fun verifyFindInPageButton() = assertUIObjectExists(findInPageButton) fun verifyAddToShortcutsButton(shouldExist: Boolean) = - assertItemContainingTextExists(addToShortcutsButton, exists = shouldExist) + assertUIObjectExists(addToShortcutsButton, exists = shouldExist) fun verifyRemoveFromShortcutsButton() = assertRemoveFromShortcutsButton() fun verifyShareTabsOverlay() = assertShareTabsOverlay() fun verifyDesktopSiteModeEnabled(isRequestDesktopSiteEnabled: Boolean) { expandMenu() - assertCheckedItemWithResIdAndTextExists(desktopSiteToggle(isRequestDesktopSiteEnabled)) + assertUIObjectExists(desktopSiteToggle(isRequestDesktopSiteEnabled)) } fun verifyPageThreeDotMainMenuItems(isRequestDesktopSiteEnabled: Boolean) { expandMenu() - assertItemContainingTextExists( + assertUIObjectExists( normalBrowsingNewTabButton, bookmarksButton, historyButton, @@ -103,18 +102,18 @@ class ThreeDotMenuMainRobot { addToHomeScreenButton, addToShortcutsButton, saveToCollectionButton, + addBookmarkButton, + desktopSiteToggle(isRequestDesktopSiteEnabled), ) - assertCheckedItemWithResIdAndTextExists(addBookmarkButton) - assertCheckedItemWithResIdAndTextExists(desktopSiteToggle(isRequestDesktopSiteEnabled)) // Swipe to second part of menu expandMenu() - assertItemContainingTextExists( + assertUIObjectExists( settingsButton(), ) if (FxNimbus.features.print.value().browserPrintEnabled) { - assertItemContainingTextExists(printContentButton) + assertUIObjectExists(printContentButton) } - assertItemWithDescriptionExists( + assertUIObjectExists( backButton, forwardButton, shareButton, @@ -123,7 +122,7 @@ class ThreeDotMenuMainRobot { } fun verifyHomeThreeDotMainMenuItems(isRequestDesktopSiteEnabled: Boolean) { - assertItemContainingTextExists( + assertUIObjectExists( bookmarksButton, historyButton, downloadsButton, @@ -135,9 +134,8 @@ class ThreeDotMenuMainRobot { helpButton, customizeHomeButton, settingsButton(), + desktopSiteToggle(isRequestDesktopSiteEnabled), ) - - assertCheckedItemWithResIdAndTextExists(desktopSiteToggle(isRequestDesktopSiteEnabled)) } private fun assertShareTabsOverlay() { @@ -156,10 +154,7 @@ class ThreeDotMenuMainRobot { fun verifyAddonAvailableInMainMenu(addonName: String) { for (i in 1..RETRY_COUNT) { try { - assertTrue( - "Addon not listed in the Add-ons sub-menu", - mDevice.findObject(UiSelector().textContains(addonName)).waitForExists(waitingTime), - ) + assertUIObjectExists(itemContainingText(addonName)) break } catch (e: AssertionError) { if (i == RETRY_COUNT) { @@ -175,6 +170,11 @@ class ThreeDotMenuMainRobot { } } + fun verifyTrackersBlockedByUblock() { + assertUIObjectExists(itemWithResId("$packageName:id/badge_text")) + assertTrue(itemWithResId("$packageName:id/badge_text").text.toInt() > 0) + } + fun clickQuit() { expandMenu() onView(withText("Quit")).click() @@ -189,8 +189,11 @@ class ThreeDotMenuMainRobot { // such as the Pixel 2, we require two swipes to display the "Settings" menu item // at the bottom. On larger devices, the second swipe is a no-op. threeDotMenuRecyclerView().perform(swipeUp()) + Log.i(TAG, "openSettings: Swiped up the main menu once") threeDotMenuRecyclerView().perform(swipeUp()) + Log.i(TAG, "openSettings: Swiped up the main menu twice") settingsButton(localizedText).click() + Log.i(TAG, "openSettings: Clicked main menu $localizedText button") SettingsRobot().interact() return SettingsRobot.Transition() @@ -198,7 +201,9 @@ class ThreeDotMenuMainRobot { fun openDownloadsManager(interact: DownloadRobot.() -> Unit): DownloadRobot.Transition { threeDotMenuRecyclerView().perform(swipeDown()) + Log.i(TAG, "openDownloadsManager: Swiped up main menu") downloadsButton.click() + Log.i(TAG, "openDownloadsManager: Clicked main menu \"DOWNLOADS\" button") DownloadRobot().interact() return DownloadRobot.Transition() @@ -218,7 +223,7 @@ class ThreeDotMenuMainRobot { mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime) bookmarksButton.click() - assertTrue(mDevice.findObject(UiSelector().resourceId("$packageName:id/bookmark_list")).waitForExists(waitingTime)) + assertUIObjectExists(itemWithResId("$packageName:id/bookmark_list")) BookmarksRobot().interact() return BookmarksRobot.Transition() @@ -299,6 +304,7 @@ class ThreeDotMenuMainRobot { fun clickShareButton(interact: ShareOverlayRobot.() -> Unit): ShareOverlayRobot.Transition { shareButton.click() + Log.i(TAG, "clickShareButton: Clicked main menu share button") mDevice.waitNotNull(Until.findObject(By.text("ALL ACTIONS")), waitingTime) ShareOverlayRobot().interact() @@ -315,8 +321,10 @@ class ThreeDotMenuMainRobot { fun refreshPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { refreshButton.also { + Log.i(TAG, "refreshPage: Looking for refresh button") it.waitForExists(waitingTime) it.click() + Log.i(TAG, "refreshPage: Clicked the refresh button") } BrowserRobot().interact() @@ -469,6 +477,16 @@ class ThreeDotMenuMainRobot { ShareOverlayRobot().interact() return ShareOverlayRobot.Transition() } + + fun clickPrintButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + threeDotMenuRecyclerView().perform(swipeUp()) + threeDotMenuRecyclerView().perform(swipeUp()) + printButton.waitForExists(waitingTime) + printButton.click() + + BrowserRobot().interact() + return BrowserRobot.Transition() + } } } private fun threeDotMenuRecyclerView() = @@ -506,13 +524,13 @@ private fun assertReaderViewAppearanceButton(visible: Boolean) { threeDotMenuRecyclerView().perform(swipeUp()) maxSwipes-- } - assertTrue(readerViewAppearanceToggle().exists()) + assertUIObjectExists(readerViewAppearanceToggle()) } else { while (!readerViewAppearanceToggle().exists() && maxSwipes != 0) { threeDotMenuRecyclerView().perform(swipeUp()) maxSwipes-- } - assertFalse(readerViewAppearanceToggle().exists()) + assertUIObjectExists(readerViewAppearanceToggle(), exists = false) } } @@ -596,3 +614,4 @@ private val backButton = itemWithDescription(getStringResource(R.string.browser_ private val forwardButton = itemWithDescription(getStringResource(R.string.browser_menu_forward)) private val shareButton = itemWithDescription(getStringResource(R.string.share_button_content_description)) private val refreshButton = itemWithDescription(getStringResource(R.string.browser_menu_refresh)) +private val printButton = itemWithText("Print") diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index 510669899e..3597f244e3 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -26,6 +26,8 @@ + + diff --git a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt index f33865ae49..f12c52d994 100644 --- a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt +++ b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt @@ -26,7 +26,6 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) { FromAbout(R.id.aboutFragment), FromTrackingProtection(R.id.trackingProtectionFragment), FromHttpsOnlyMode(R.id.httpsOnlyFragment), - FromCookieBanner(R.id.cookieBannerFragment), FromTrackingProtectionDialog(R.id.trackingProtectionPanelDialogFragment), FromSavedLoginsFragment(R.id.savedLoginsFragment), FromAddNewDeviceFragment(R.id.addNewDeviceFragment), diff --git a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index ec2da5dc43..180a9561e0 100644 --- a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -50,7 +50,7 @@ object FeatureFlags { /** * Enables compose on the tabs tray items. */ - val composeTabsTray = Config.channel.isNightlyOrDebug + val composeTabsTray = Config.channel.isNightlyOrDebug || Config.channel.isBeta /** * Enables compose on the top sites. @@ -73,4 +73,9 @@ object FeatureFlags { * Allows users to enable Firefox Suggest. */ const val fxSuggest = true + + /** + * Enable Meta attribution. + */ + const val metaAttributionEnabled = true } diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index dd53eb09f3..3aadd58031 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -374,9 +374,11 @@ open class FenixApplication : LocaleAwareApplication(), Provider { // If Firefox Suggest is enabled, register a worker to periodically ingest // new search suggestions. The worker requires us to have called // `GlobalFxSuggestDependencyProvider.initialize`, which we did before - // scheduling these tasks. + // scheduling these tasks. When disabled we stop the periodic work. if (settings().enableFxSuggest) { components.fxSuggest.ingestionScheduler.startPeriodicIngestion() + } else { + components.fxSuggest.ingestionScheduler.stopPeriodicIngestion() } } } @@ -877,6 +879,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider { componentOptedOut.set(!settings.isReviewQualityCheckEnabled) nimbusDisabledShopping.set(!FxNimbus.features.shoppingExperience.value().enabled) userHasOnboarded.set(settings.reviewQualityCheckOptInTimeInMillis != 0L) + disabledAds.set(!settings.isReviewQualityCheckProductRecommendationsEnabled) } } diff --git a/app/src/main/java/org/mozilla/fenix/FenixLogSink.kt b/app/src/main/java/org/mozilla/fenix/FenixLogSink.kt index 03f91d7468..4037c08607 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixLogSink.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixLogSink.kt @@ -11,8 +11,8 @@ import mozilla.components.support.base.log.sink.LogSink /** * Fenix [LogSink] implementation that writes to Android's log, depending on settings. * - * @property logsDebug If set to false, removes logging of debug logs. - * @property androidLogSink an [AndroidLogSink] that writes to Android's log. + * @param logsDebug If set to false, removes logging of debug logs. + * @param androidLogSink an [AndroidLogSink] that writes to Android's log. */ class FenixLogSink( private val logsDebug: Boolean = true, diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 341658f2f6..69d8ffb8d9 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -140,14 +140,12 @@ import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.perf.StartupTypeTelemetry import org.mozilla.fenix.search.SearchDialogFragmentDirections import org.mozilla.fenix.session.PrivateNotificationService -import org.mozilla.fenix.settings.CookieBannersFragmentDirections import org.mozilla.fenix.settings.HttpsOnlyFragmentDirections import org.mozilla.fenix.settings.SettingsFragmentDirections import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections import org.mozilla.fenix.settings.about.AboutFragmentDirections import org.mozilla.fenix.settings.logins.fragment.LoginDetailFragmentDirections import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirections -import org.mozilla.fenix.settings.quicksettings.protections.cookiebanners.dialog.CookieBannerReEngagementDialogUtils import org.mozilla.fenix.settings.search.SaveSearchEngineFragmentDirections import org.mozilla.fenix.settings.search.SearchEngineFragmentDirections import org.mozilla.fenix.settings.studies.StudiesFragmentDirections @@ -531,10 +529,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { // and the user changes the system language // More details here: https://github.com/mozilla-mobile/fenix/pull/27793#discussion_r1029892536 components.core.store.dispatch(SearchAction.RefreshSearchEnginesAction) - CookieBannerReEngagementDialogUtils.tryToEnableDetectOnlyModeIfNeeded( - components.settings, - components.core.engine.settings, - ) } override fun onStart() { @@ -1027,8 +1021,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { HistoryMetadataGroupFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromTrackingProtectionExceptions -> TrackingProtectionExceptionsFragmentDirections.actionGlobalBrowser(customTabSessionId) - BrowserDirection.FromCookieBanner -> - CookieBannersFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromHttpsOnlyMode -> HttpsOnlyFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromAbout -> diff --git a/app/src/main/java/org/mozilla/fenix/ServiceWorkerSupportFeature.kt b/app/src/main/java/org/mozilla/fenix/ServiceWorkerSupportFeature.kt index a1e7a922c2..574d77831c 100644 --- a/app/src/main/java/org/mozilla/fenix/ServiceWorkerSupportFeature.kt +++ b/app/src/main/java/org/mozilla/fenix/ServiceWorkerSupportFeature.kt @@ -18,7 +18,7 @@ import org.mozilla.fenix.ext.components * * Will automatically register callbacks for service workers requests and cleanup when [homeActivity] is destroyed. * - * @property homeActivity [HomeActivity] used for navigating to browser or accessing various app components. + * @param homeActivity [HomeActivity] used for navigating to browser or accessing various app components. */ class ServiceWorkerSupportFeature( private val homeActivity: HomeActivity, diff --git a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt index 03e1b6b832..f810667c45 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -17,7 +17,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.accessibility.AccessibilityManager -import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.annotation.CallSuper import androidx.annotation.VisibleForTesting @@ -76,6 +75,7 @@ import mozilla.components.feature.prompts.PromptFeature import mozilla.components.feature.prompts.PromptFeature.Companion.PIN_REQUEST import mozilla.components.feature.prompts.address.AddressDelegate import mozilla.components.feature.prompts.creditcard.CreditCardDelegate +import mozilla.components.feature.prompts.dialog.FullScreenNotificationDialog import mozilla.components.feature.prompts.identitycredential.DialogColors import mozilla.components.feature.prompts.identitycredential.DialogColorsProvider import mozilla.components.feature.prompts.login.LoginDelegate @@ -1493,9 +1493,11 @@ abstract class BaseBrowserFragment : if (inFullScreen) { // Close find in page bar if opened findInPageIntegration.onBackPressed() - Toast - .makeText(requireContext(), R.string.full_screen_notification, Toast.LENGTH_SHORT) - .show() + + FullScreenNotificationDialog(R.layout.full_screen_notification_dialog).show( + parentFragmentManager, + ) + activity?.enterImmersiveMode() (view as? SwipeGestureLayout)?.isSwipeEnabled = false browserToolbarView.collapse() diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index c21eb9dffb..f93d8c455d 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -16,14 +16,11 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.TabSessionState -import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.thumbnails.BrowserThumbnails import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.concept.engine.permission.SitePermissions @@ -32,11 +29,9 @@ import mozilla.components.feature.contextmenu.ContextMenuCandidate import mozilla.components.feature.readerview.ReaderViewFeature import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tabs.WindowFeature -import mozilla.components.lib.state.ext.consumeFlow import mozilla.components.service.glean.private.NoExtras import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.ViewBoundFeatureWrapper -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged import org.mozilla.fenix.GleanMetrics.ReaderMode import org.mozilla.fenix.GleanMetrics.Shopping import org.mozilla.fenix.HomeActivity @@ -52,7 +47,6 @@ import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.ext.settings import org.mozilla.fenix.nimbus.FxNimbus -import org.mozilla.fenix.settings.quicksettings.protections.cookiebanners.dialog.CookieBannerReEngagementDialogUtils import org.mozilla.fenix.settings.quicksettings.protections.cookiebanners.getCookieBannerUIMode import org.mozilla.fenix.shopping.DefaultShoppingExperienceFeature import org.mozilla.fenix.shopping.ReviewQualityCheckFeature @@ -209,10 +203,6 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { ) } - if (!context.settings().shouldUseCookieBanner && !context.settings().userOptOutOfReEngageCookieBannerDialog) { - observeCookieBannerHandlingState(context.components.core.store) - } - standardSnackbarErrorBinding.set( feature = StandardSnackbarErrorBinding( requireActivity(), @@ -233,9 +223,10 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { image = AppCompatResources.getDrawable( context, R.drawable.mozac_ic_translate_24, - )!!, - imageSelected = - AppCompatResources.getDrawable( + )!!.apply { + setTint(ContextCompat.getColor(context, R.color.fx_mobile_text_color_primary)) + }, + imageSelected = AppCompatResources.getDrawable( context, R.drawable.mozac_ic_translate_24, )!!, @@ -611,22 +602,4 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { internal fun updateLastBrowseActivity() { requireContext().settings().lastBrowseActivity = System.currentTimeMillis() } - - private fun observeCookieBannerHandlingState(store: BrowserStore) { - consumeFlow(store) { flow -> - flow.mapNotNull { state -> - state.findCustomTabOrSelectedTab(customTabSessionId) - }.ifAnyChanged { tab -> - arrayOf( - tab.cookieBanner, - ) - }.collect { - CookieBannerReEngagementDialogUtils.tryToShowReEngagementDialog( - settings = requireContext().settings(), - status = it.cookieBanner, - navController = findNavController(), - ) - } - } - } } diff --git a/app/src/main/java/org/mozilla/fenix/browser/infobanner/DynamicInfoBanner.kt b/app/src/main/java/org/mozilla/fenix/browser/infobanner/DynamicInfoBanner.kt index 41a780c41a..132369dcba 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/infobanner/DynamicInfoBanner.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/infobanner/DynamicInfoBanner.kt @@ -12,7 +12,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout * [InfoBanner] that will automatically scroll with the top [BrowserToolbar]. * Only to be used with [BrowserToolbar]s placed at the top of the screen. * - * @property context A [Context] for accessing system resources. + * @param context A [Context] for accessing system resources. * @param container The layout where the banner will be shown. * @property shouldScrollWithTopToolbar whether to follow the Y translation of the top toolbar or not. * @param message The message displayed in the banner. diff --git a/app/src/main/java/org/mozilla/fenix/browser/infobanner/InfoBanner.kt b/app/src/main/java/org/mozilla/fenix/browser/infobanner/InfoBanner.kt index 377fc4a0f7..1cefa8529a 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/infobanner/InfoBanner.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/infobanner/InfoBanner.kt @@ -17,14 +17,14 @@ import org.mozilla.fenix.ext.settings * Displays an Info Banner in the specified container with a message and an optional action. * The container can be a placeholder layout inserted in the original screen, or an existing layout. * - * @property context A [Context] for accessing system resources. - * @property container The layout where the banner will be shown. - * @property message The message displayed in the banner. - * @property dismissText The text on the dismiss button. - * @property actionText The text on the action to perform button. - * @property dismissByHiding Whether or not to hide the banner when dismissed. + * @param context A [Context] for accessing system resources. + * @param container The layout where the banner will be shown. + * @param message The message displayed in the banner. + * @param dismissText The text on the dismiss button. + * @param actionText The text on the action to perform button. + * @param dismissByHiding Whether or not to hide the banner when dismissed. * @property dismissAction Optional callback invoked when the user dismisses the banner. - * @property actionToPerform The action to be performed on action button press. + * @param actionToPerform The action to be performed on action button press. */ @SuppressWarnings("LongParameterList") open class InfoBanner( diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationController.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationController.kt index 88fec6e858..76fc82868a 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationController.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationController.kt @@ -64,11 +64,11 @@ fun List.toTabSessionStateList(store: BrowserStore): List } /** - * @property store Store used to hold in-memory collection state. - * @property browserStore The global `BrowserStore` instance. - * @property dismiss Callback to dismiss the collection creation dialog. - * @property tabCollectionStorage Storage used to save tab collections to disk. - * @property scope Coroutine scope to launch coroutines. + * @param store Store used to hold in-memory collection state. + * @param browserStore The global `BrowserStore` instance. + * @param dismiss Callback to dismiss the collection creation dialog. + * @param tabCollectionStorage Storage used to save tab collections to disk. + * @param scope Coroutine scope to launch coroutines. */ class DefaultCollectionCreationController( private val store: CollectionCreationStore, diff --git a/app/src/main/java/org/mozilla/fenix/components/AccountAbnormalities.kt b/app/src/main/java/org/mozilla/fenix/components/AccountAbnormalities.kt index b46caa0253..cb39ffe436 100644 --- a/app/src/main/java/org/mozilla/fenix/components/AccountAbnormalities.kt +++ b/app/src/main/java/org/mozilla/fenix/components/AccountAbnormalities.kt @@ -49,7 +49,7 @@ internal abstract class AbnormalFxaEvent : Exception() { * See [AbnormalFxaEvent] for types of abnormal events this class detects. * * @param context An Android [Context]. - * @property crashReporter An instance of [CrashReporter] used for reporting detected abnormalities. + * @param crashReporter An instance of [CrashReporter] used for reporting detected abnormalities. * @param strictMode An instance of [StrictModeManager] used to manage strict mode settings for the * application. */ diff --git a/app/src/main/java/org/mozilla/fenix/components/Core.kt b/app/src/main/java/org/mozilla/fenix/components/Core.kt index aece11e5ac..b9a73a5df0 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Core.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -142,10 +142,11 @@ class Core( R.color.fx_mobile_layer_color_1, ), httpsOnlyMode = context.settings().getHttpsOnlyMode(), - cookieBannerHandlingModePrivateBrowsing = context.settings().getCookieBannerHandlingPrivateMode(), cookieBannerHandlingMode = context.settings().getCookieBannerHandling(), - cookieBannerHandlingDetectOnlyMode = context.settings() - .shouldShowCookieBannerReEngagementDialog(), + cookieBannerHandlingModePrivateBrowsing = context.settings().getCookieBannerHandlingPrivateMode(), + cookieBannerHandlingDetectOnlyMode = context.settings().shouldEnableCookieBannerDetectOnly, + cookieBannerHandlingGlobalRules = context.settings().shouldEnableCookieBannerGlobalRules, + cookieBannerHandlingGlobalRulesSubFrames = context.settings().shouldEnableCookieBannerGlobalRulesSubFrame, ) GeckoEngine( diff --git a/app/src/main/java/org/mozilla/fenix/components/FindInPageIntegration.kt b/app/src/main/java/org/mozilla/fenix/components/FindInPageIntegration.kt index 1f7af312cf..fbffa7d3b3 100644 --- a/app/src/main/java/org/mozilla/fenix/components/FindInPageIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/components/FindInPageIntegration.kt @@ -22,12 +22,12 @@ import org.mozilla.fenix.components.FindInPageIntegration.ToolbarInfo /** * BrowserFragment delegate to handle all layout updates needed to show or hide the find in page bar. * - * @property store The [BrowserStore] used to look up the current selected tab. - * @property sessionId ID of the [store] session in which the query will be performed. - * @property view The [FindInPageBar] view to display. - * @property engineView the browser in which the queries will be made and which needs to be better positioned + * @param store The [BrowserStore] used to look up the current selected tab. + * @param sessionId ID of the [store] session in which the query will be performed. + * @param view The [FindInPageBar] view to display. + * @param engineView the browser in which the queries will be made and which needs to be better positioned * to suit the find in page bar. - * @property toolbarInfo [ToolbarInfo] used to configure the [BrowserToolbar] while the find in page bar is shown. + * @param toolbarInfo [ToolbarInfo] used to configure the [BrowserToolbar] while the find in page bar is shown. */ class FindInPageIntegration( private val store: BrowserStore, diff --git a/app/src/main/java/org/mozilla/fenix/components/FxaServer.kt b/app/src/main/java/org/mozilla/fenix/components/FxaServer.kt index 3347dc2a7e..b52ee58df7 100644 --- a/app/src/main/java/org/mozilla/fenix/components/FxaServer.kt +++ b/app/src/main/java/org/mozilla/fenix/components/FxaServer.kt @@ -5,8 +5,8 @@ package org.mozilla.fenix.components import android.content.Context +import mozilla.appservices.fxaclient.FxaServer import mozilla.components.service.fxa.ServerConfig -import mozilla.components.service.fxa.ServerConfig.Server import org.mozilla.fenix.Config import org.mozilla.fenix.ext.settings @@ -20,18 +20,19 @@ object FxaServer { fun config(context: Context): ServerConfig { // If a server override is configured, use that. Otherwise: - // - for all channels other than Mozilla Online, use Server.Release. - // - for Mozilla Online channel, if domestic server is allowed, use Server.CHINA; otherwise, use Server.RELEASE + // - for all channels other than Mozilla Online, use FxaServer.Release. + // - for Mozilla Online channel, if domestic server is allowed, use FxaServer.China; otherwise, + // use FxaServer.Release val serverOverride = context.settings().overrideFxAServer val tokenServerOverride = context.settings().overrideSyncTokenServer.ifEmpty { null } if (serverOverride.isEmpty()) { val releaseServer = if (Config.channel.isMozillaOnline && context.settings().allowDomesticChinaFxaServer) { - Server.CHINA + FxaServer.China } else { - Server.RELEASE + FxaServer.Release } return ServerConfig(releaseServer, CLIENT_ID, REDIRECT_URL, tokenServerOverride) } - return ServerConfig(serverOverride, CLIENT_ID, REDIRECT_URL, tokenServerOverride) + return ServerConfig(FxaServer.Custom(serverOverride), CLIENT_ID, REDIRECT_URL, tokenServerOverride) } } diff --git a/app/src/main/java/org/mozilla/fenix/components/StoreProvider.kt b/app/src/main/java/org/mozilla/fenix/components/StoreProvider.kt index 9508d611de..dbc278cfb5 100644 --- a/app/src/main/java/org/mozilla/fenix/components/StoreProvider.kt +++ b/app/src/main/java/org/mozilla/fenix/components/StoreProvider.kt @@ -10,19 +10,31 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.get +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CoroutineScope import mozilla.components.lib.state.Store /** * Generic ViewModel to wrap a State object for state restoration. * - * @property store [Store] instance attached to [ViewModel]. + * @param createStore Function that creates a [Store] instance that receives [CoroutineScope] param + * that's tied to the lifecycle of the [StoreProvider] i.e it will cancel when + * [StoreProvider.onCleared] is called. */ class StoreProvider>( - val store: T, + createStore: (CoroutineScope) -> T, ) : ViewModel() { + @VisibleForTesting + internal val store: T = createStore(viewModelScope) + companion object { - fun > get(owner: ViewModelStoreOwner, createStore: () -> T): T { + /** + * Returns an existing [Store] instance or creates a new one scoped to a + * [ViewModelStoreOwner]. + * @see [ViewModelProvider.get]. + */ + fun > get(owner: ViewModelStoreOwner, createStore: (CoroutineScope) -> T): T { val factory = StoreProviderFactory(createStore) val viewModel: StoreProvider = ViewModelProvider(owner, factory).get() return viewModel.store @@ -33,27 +45,43 @@ class StoreProvider>( /** * ViewModel factory to create [StoreProvider] instances. * - * @property createStore Callback to create a new [Store], used when the [ViewModel] is first created. + * @param createStore Callback to create a new [Store], used when the [ViewModel] is first created. */ @VisibleForTesting class StoreProviderFactory>( - private val createStore: () -> T, + private val createStore: (CoroutineScope) -> T, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): VM { - return StoreProvider(createStore()) as VM + return StoreProvider(createStore) as VM } } /** * Helper function for lazy creation of a [Store] instance scoped to a [ViewModelStoreOwner]. * - * @param createStore Function that creates a [Store] instance. + * @param createStore Function that creates a [Store] instance that receives [CoroutineScope] param + * that's tied to the lifecycle of the [StoreProvider] i.e it will cancel when + * [StoreProvider.onCleared] is called. + * + * Example: + * ``` + * val store by lazy { scope -> + * MyStore( + * middleware = listOf( + * MyMiddleware( + * settings = requireComponents.settings, + * ... + * scope = scope, + * ), + * ) + * ) + * } */ @MainThread fun > ViewModelStoreOwner.lazyStore( - createStore: () -> T, + createStore: (CoroutineScope) -> T, ): Lazy { return lazy(mode = LazyThreadSafetyMode.NONE) { StoreProvider.get(this, createStore) diff --git a/app/src/main/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactory.kt b/app/src/main/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactory.kt index 3ac7a7d8d2..d795b59a00 100644 --- a/app/src/main/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactory.kt +++ b/app/src/main/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactory.kt @@ -54,6 +54,7 @@ class TrackingProtectionPolicyFactory( cookiePolicy = getCustomCookiePolicy(), trackingCategories = getCustomTrackingCategories(), cookiePurging = getCustomCookiePurgingPolicy(), + strictSocialTrackingProtection = settings.blockTrackingContentInCustomTrackingProtection, ).let { if (settings.blockTrackingContentSelectionInCustomTrackingProtection == "private") { it.forPrivateSessionsOnly() diff --git a/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt b/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt index 9280e9ca7d..6a2bb18e0b 100644 --- a/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt +++ b/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt @@ -13,8 +13,8 @@ import mozilla.components.service.nimbus.messaging.MessageSurfaceId import mozilla.components.service.pocket.PocketStory import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory import org.mozilla.fenix.browser.StandardSnackbarError +import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.components.AppStore -import org.mozilla.fenix.home.Mode import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory import org.mozilla.fenix.home.recentbookmarks.RecentBookmark @@ -42,7 +42,7 @@ sealed class AppAction : Action { data class Change( val topSites: List, - val mode: Mode, + val mode: BrowsingMode, val collections: List, val showCollectionPlaceholder: Boolean, val recentTabs: List, @@ -56,7 +56,7 @@ sealed class AppAction : Action { AppAction() data class CollectionsChange(val collections: List) : AppAction() - data class ModeChange(val mode: Mode) : AppAction() + data class ModeChange(val mode: BrowsingMode) : AppAction() data class TopSitesChange(val topSites: List) : AppAction() data class RecentTabsChange(val recentTabs: List) : AppAction() data class RemoveRecentTab(val recentTab: RecentTab) : AppAction() @@ -228,14 +228,27 @@ sealed class AppAction : Action { data class ShoppingSheetStateUpdated(val expanded: Boolean) : ShoppingAction() /** - * [ShoppingAction] used to add a product to a set of products that are being analysed. + * [ShoppingAction] used to update the expansion state of the highlights card. */ - data class AddToProductAnalysed(val productPageUrl: String) : ShoppingAction() + data class HighlightsCardExpanded( + val productPageUrl: String, + val expanded: Boolean, + ) : ShoppingAction() /** - * [ShoppingAction] used to remove a product from the set of products that are being - * analysed. + * [ShoppingAction] used to update the expansion state of the info card. */ - data class RemoveFromProductAnalysed(val productPageUrl: String) : ShoppingAction() + data class InfoCardExpanded( + val productPageUrl: String, + val expanded: Boolean, + ) : ShoppingAction() + + /** + * [ShoppingAction] used to update the expansion state of the settings card. + */ + data class SettingsCardExpanded( + val productPageUrl: String, + val expanded: Boolean, + ) : ShoppingAction() } } diff --git a/app/src/main/java/org/mozilla/fenix/components/appstate/AppState.kt b/app/src/main/java/org/mozilla/fenix/components/appstate/AppState.kt index 7bafe9b837..77edd2d016 100644 --- a/app/src/main/java/org/mozilla/fenix/components/appstate/AppState.kt +++ b/app/src/main/java/org/mozilla/fenix/components/appstate/AppState.kt @@ -13,9 +13,9 @@ import mozilla.components.service.pocket.PocketStory import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory import org.mozilla.fenix.browser.StandardSnackbarError +import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.components.appstate.shopping.ShoppingState import org.mozilla.fenix.home.HomeFragment -import org.mozilla.fenix.home.Mode import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory import org.mozilla.fenix.home.recentbookmarks.RecentBookmark @@ -37,7 +37,7 @@ import org.mozilla.fenix.wallpapers.WallpaperState * @property collections The list of [TabCollection] to display in the [HomeFragment]. * @property expandedCollections A set containing the ids of the [TabCollection] that are expanded * in the [HomeFragment]. - * @property mode The state of the [HomeFragment] UI. + * @property mode Whether the app is in private browsing mode. * @property topSites The list of [TopSite] in the [HomeFragment]. * @property showCollectionPlaceholder If true, shows a placeholder when there are no collections. * @property recentTabs The list of recent [RecentTab] in the [HomeFragment]. @@ -63,7 +63,7 @@ data class AppState( val nonFatalCrashes: List = emptyList(), val collections: List = emptyList(), val expandedCollections: Set = emptySet(), - val mode: Mode = Mode.Normal, + val mode: BrowsingMode = BrowsingMode.Normal, val topSites: List = emptyList(), val showCollectionPlaceholder: Boolean = false, val recentTabs: List = emptyList(), diff --git a/app/src/main/java/org/mozilla/fenix/components/appstate/shopping/ShoppingState.kt b/app/src/main/java/org/mozilla/fenix/components/appstate/shopping/ShoppingState.kt index 98dbf59395..d2287bd55e 100644 --- a/app/src/main/java/org/mozilla/fenix/components/appstate/shopping/ShoppingState.kt +++ b/app/src/main/java/org/mozilla/fenix/components/appstate/shopping/ShoppingState.kt @@ -7,11 +7,25 @@ package org.mozilla.fenix.components.appstate.shopping /** * State for shopping feature that's required to live the lifetime of a session. * - * @property productsInAnalysis Set of product ids that are currently being analysed or were being - * analysed when the sheet was closed. * @property shoppingSheetExpanded Boolean indicating if the shopping sheet is expanded and visible. + * @property productCardState Map of product url to [CardState] that contains the state of different + * cards in the shopping sheet. */ data class ShoppingState( - val productsInAnalysis: Set = emptySet(), val shoppingSheetExpanded: Boolean? = null, -) + val productCardState: Map = emptyMap(), +) { + + /** + * State for different cards in the shopping sheet for a product. + * + * @property isHighlightsExpanded Whether or not the highlights card is expanded. + * @property isInfoExpanded Whether or not the info card is expanded. + * @property isSettingsExpanded Whether or not the settings card is expanded. + */ + data class CardState( + val isHighlightsExpanded: Boolean = false, + val isInfoExpanded: Boolean = false, + val isSettingsExpanded: Boolean = false, + ) +} diff --git a/app/src/main/java/org/mozilla/fenix/components/appstate/shopping/ShoppingStateReducer.kt b/app/src/main/java/org/mozilla/fenix/components/appstate/shopping/ShoppingStateReducer.kt index 7ee29faf74..f8c5b2b59d 100644 --- a/app/src/main/java/org/mozilla/fenix/components/appstate/shopping/ShoppingStateReducer.kt +++ b/app/src/main/java/org/mozilla/fenix/components/appstate/shopping/ShoppingStateReducer.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.components.appstate.shopping import org.mozilla.fenix.components.appstate.AppAction.ShoppingAction import org.mozilla.fenix.components.appstate.AppState +import org.mozilla.fenix.components.appstate.shopping.ShoppingState.CardState /** * Reducer for the shopping state that handles [ShoppingAction]s. @@ -23,16 +24,48 @@ internal object ShoppingStateReducer { ), ) - is ShoppingAction.AddToProductAnalysed -> state.copy( - shoppingState = state.shoppingState.copy( - productsInAnalysis = state.shoppingState.productsInAnalysis + action.productPageUrl, - ), - ) + is ShoppingAction.HighlightsCardExpanded -> { + val updatedValue = + state.shoppingState.productCardState[action.productPageUrl]?.copy( + isHighlightsExpanded = action.expanded, + ) ?: CardState(isHighlightsExpanded = action.expanded) - is ShoppingAction.RemoveFromProductAnalysed -> state.copy( - shoppingState = state.shoppingState.copy( - productsInAnalysis = state.shoppingState.productsInAnalysis - action.productPageUrl, - ), - ) + state.copy( + shoppingState = state.shoppingState.updateProductCardState( + action.productPageUrl, + updatedValue, + ), + ) + } + + is ShoppingAction.InfoCardExpanded -> { + val updatedValue = + state.shoppingState.productCardState[action.productPageUrl]?.copy(isInfoExpanded = action.expanded) + ?: CardState(isInfoExpanded = action.expanded) + + state.copy( + shoppingState = state.shoppingState.updateProductCardState( + action.productPageUrl, + updatedValue, + ), + ) + } + + is ShoppingAction.SettingsCardExpanded -> { + val updatedValue = + state.shoppingState.productCardState[action.productPageUrl]?.copy( + isSettingsExpanded = action.expanded, + ) ?: CardState(isSettingsExpanded = action.expanded) + + state.copy( + shoppingState = state.shoppingState.updateProductCardState( + action.productPageUrl, + updatedValue, + ), + ) + } } + + private fun ShoppingState.updateProductCardState(key: String, value: CardState): ShoppingState = + copy(productCardState = productCardState + (key to value)) } diff --git a/app/src/main/java/org/mozilla/fenix/components/bookmarks/BookmarksUseCase.kt b/app/src/main/java/org/mozilla/fenix/components/bookmarks/BookmarksUseCase.kt index 500f1dd8f0..19a8c856b6 100644 --- a/app/src/main/java/org/mozilla/fenix/components/bookmarks/BookmarksUseCase.kt +++ b/app/src/main/java/org/mozilla/fenix/components/bookmarks/BookmarksUseCase.kt @@ -51,8 +51,8 @@ class BookmarksUseCase( /** * Uses for retrieving recently added bookmarks. * - * @property bookmarksStorage [BookmarksStorage] to retrieve the bookmark data. - * @property historyStorage Optional [HistoryStorage] to retrieve the preview image of a visited + * @param bookmarksStorage [BookmarksStorage] to retrieve the bookmark data. + * @param historyStorage Optional [HistoryStorage] to retrieve the preview image of a visited * page associated with a bookmark. */ class RetrieveRecentBookmarksUseCase internal constructor( diff --git a/app/src/main/java/org/mozilla/fenix/components/history/PagedHistoryProvider.kt b/app/src/main/java/org/mozilla/fenix/components/history/PagedHistoryProvider.kt index 96d28d3e28..b972ede85c 100644 --- a/app/src/main/java/org/mozilla/fenix/components/history/PagedHistoryProvider.kt +++ b/app/src/main/java/org/mozilla/fenix/components/history/PagedHistoryProvider.kt @@ -81,7 +81,7 @@ interface PagedHistoryProvider { } /** - * @property historyStorage An instance [PlacesHistoryStorage] that provides read/write methods for + * @param historyStorage An instance [PlacesHistoryStorage] that provides read/write methods for * history data. */ class DefaultPagedHistoryProvider( diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/InstallReferrerMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/InstallReferrerMetricsService.kt index 3e6415240f..736e237ce1 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/InstallReferrerMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/InstallReferrerMetricsService.kt @@ -5,26 +5,90 @@ package org.mozilla.fenix.components.metrics import android.content.Context -import android.net.UrlQuerySanitizer import android.os.RemoteException -import androidx.annotation.VisibleForTesting +import com.android.installreferrer.api.InstallReferrerClient +import com.android.installreferrer.api.InstallReferrerStateListener +import mozilla.components.support.base.log.logger.Logger +import org.json.JSONException +import org.json.JSONObject +import org.mozilla.fenix.FeatureFlags +import org.mozilla.fenix.GleanMetrics.MetaAttribution import org.mozilla.fenix.GleanMetrics.PlayStoreAttribution import org.mozilla.fenix.ext.settings import org.mozilla.fenix.utils.Settings +import java.io.UnsupportedEncodingException import java.net.URLDecoder /** * A metrics service used to derive the UTM parameters with the Google Play Install Referrer library. * - * At first startup, the [UTMParams] are derived from the install referrer URL and stored in settings. + * At first startup, the [UTMParams] and/or [MetaParams] are derived from the install referrer URL + * and stored in settings. */ class InstallReferrerMetricsService(private val context: Context) : MetricsService { + private val logger = Logger("InstallReferrerMetricsService") override val type = MetricServiceType.Marketing override fun start() { if (context.settings().utmParamsKnown) { return } + + val timerId = PlayStoreAttribution.attributionTime.start() + val client = InstallReferrerClient.newBuilder(context).build() + referrerClient = client + + client.startConnection( + object : InstallReferrerStateListener { + override fun onInstallReferrerSetupFinished(responseCode: Int) { + PlayStoreAttribution.attributionTime.stopAndAccumulate(timerId) + when (responseCode) { + InstallReferrerClient.InstallReferrerResponse.OK -> { + // Connection established. + val installReferrerResponse = try { + client.installReferrer.installReferrer + } catch (e: RemoteException) { + // We can't do anything about this. + logger.error("Failed to retrieve install referrer response", e) + null + } + + if (installReferrerResponse.isNullOrBlank()) { + return + } + + PlayStoreAttribution.installReferrerResponse.set(installReferrerResponse) + + val utmParams = UTMParams.parseUTMParameters(installReferrerResponse) + if (FeatureFlags.metaAttributionEnabled) { + MetaParams.extractMetaAttribution(utmParams.content) + ?.recordMetaAttribution() + } + + utmParams.recordInstallReferrer(context.settings()) + context.settings().utmParamsKnown = true + } + + InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED -> { + // API not available on the current Play Store app. + context.settings().utmParamsKnown = true + } + + InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE -> { + // Connection couldn't be established. + } + } + // End the connection, and null out the client. + stop() + } + + override fun onInstallReferrerServiceDisconnected() { + // Try to restart the connection on the next request to + // Google Play by calling the startConnection() method. + referrerClient = null + } + }, + ) } override fun stop() { @@ -33,36 +97,6 @@ class InstallReferrerMetricsService(private val context: Context) : MetricsServi override fun track(event: Event) = Unit override fun shouldTrack(event: Event): Boolean = false - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun recordInstallReferrer(settings: Settings, url: String?) { - if (url.isNullOrBlank()) { - return - } - val params = UTMParams.fromURLString(url) - if (params == null || params.isEmpty()) { - return - } - params.intoSettings(settings) - - params.apply { - source?.let { - PlayStoreAttribution.source.set(it) - } - medium?.let { - PlayStoreAttribution.medium.set(it) - } - campaign?.let { - PlayStoreAttribution.campaign.set(it) - } - content?.let { - PlayStoreAttribution.content.set(it) - } - term?.let { - PlayStoreAttribution.term.set(it) - } - } - } } /** @@ -86,61 +120,44 @@ class InstallReferrerMetricsService(private val context: Context) : MetricsServi * which version is more effective. */ data class UTMParams( - val source: String?, - val medium: String?, - val campaign: String?, - val term: String?, - val content: String?, + val source: String, + val medium: String, + val campaign: String, + val content: String, + val term: String, ) { + companion object { const val UTM_SOURCE = "utm_source" const val UTM_MEDIUM = "utm_medium" const val UTM_CAMPAIGN = "utm_campaign" - const val UTM_TERM = "utm_term" const val UTM_CONTENT = "utm_content" + const val UTM_TERM = "utm_term" /** - * Try and unpack the referrer URL by successively URLDecoding the URL. - * - * Once the url ceases to decode anymore, it gives up. + * Try and unpack the install referrer response. */ - fun fromURLString(urlString: String): UTMParams? { - // Look for the first time 'utm_' is detected, after the first '?'. - val utmIndex = urlString.indexOf("utm_", urlString.indexOf('?')) - if (utmIndex < 0) { - return null - } - var url = urlString.substring(utmIndex) - while (true) { - val params = fromQueryString(url) - if (!params.isEmpty()) { - return params - } - val newValue = URLDecoder.decode(url, "UTF-8") - if (newValue == url) { - break + fun parseUTMParameters(installReferrerResponse: String): UTMParams { + val utmParams = mutableMapOf() + val params = installReferrerResponse.split("&") + + for (param in params) { + val keyValue = param.split("=") + if (keyValue.size == 2) { + val key = keyValue[0] + val value = keyValue[1] + utmParams[key] = value } - url = newValue } - return null - } - /** - * Derive a set of UTM parameters from a string URL. - */ - fun fromQueryString(queryString: String): UTMParams = - with(UrlQuerySanitizer()) { - allowUnregisteredParamaters = true - unregisteredParameterValueSanitizer = UrlQuerySanitizer.getUrlAndSpaceLegal() - parseQuery(queryString) - UTMParams( - source = getValue(UTM_SOURCE), - medium = getValue(UTM_MEDIUM), - campaign = getValue(UTM_CAMPAIGN), - term = getValue(UTM_TERM), - content = getValue(UTM_CONTENT), - ) - } + return UTMParams( + source = utmParams[UTM_SOURCE] ?: "", + medium = utmParams[UTM_MEDIUM] ?: "", + campaign = utmParams[UTM_CAMPAIGN] ?: "", + content = utmParams[UTM_CONTENT] ?: "", + term = utmParams[UTM_TERM] ?: "", + ) + } /** * Derive the set of UTM parameters stored in Settings. @@ -151,8 +168,8 @@ data class UTMParams( source = utmSource, medium = utmMedium, campaign = utmCampaign, - term = utmTerm, content = utmContent, + term = utmTerm, ) } } @@ -162,22 +179,140 @@ data class UTMParams( */ fun intoSettings(settings: Settings) { with(settings) { - utmSource = source ?: "" - utmMedium = medium ?: "" - utmCampaign = campaign ?: "" - utmTerm = term ?: "" - utmContent = content ?: "" + utmSource = source + utmMedium = medium + utmCampaign = campaign + utmTerm = term + utmContent = content } } /** - * Return [true] if none of the utm params are set. + * Check if this UTM param is empty + * + * @Return [Boolean] true if none of the utm params are set. */ fun isEmpty(): Boolean { - return source.isNullOrBlank() && - medium.isNullOrBlank() && - campaign.isNullOrBlank() && - term.isNullOrBlank() && - content.isNullOrBlank() + return source.isBlank() && + medium.isBlank() && + campaign.isBlank() && + term.isBlank() && + content.isBlank() + } + + /** + * record UTM params into settings and telemetry + * + * @param settings [Settings] application settings. + */ + fun recordInstallReferrer(settings: Settings) { + if (isEmpty()) { + return + } + intoSettings(settings) + + PlayStoreAttribution.source.set(source) + PlayStoreAttribution.medium.set(medium) + PlayStoreAttribution.campaign.set(campaign) + PlayStoreAttribution.content.set(content) + PlayStoreAttribution.term.set(term) + } +} + +/** + * Descriptions of Meta attribution parameters comes from + * https://developers.facebook.com/docs/marketing-api/reference/ad-campaign#fields + * + * @property app the ID of application in the referrer response. + * @property t the value of user interaction in the referrer response. + * @property data the encrypted data in the referrer response. + * @property nonce the nonce for decrypting [data] in the referrer response. + */ +data class MetaParams( + val app: String, + val t: String, + val data: String, + val nonce: String, +) { + companion object { + private val logger = Logger("MetaParams") + private const val APP = "app" + private const val T = "t" + private const val SOURCE = "source" + private const val DATA = "data" + private const val NONCE = "nonce" + + @Suppress("ReturnCount") + internal fun extractMetaAttribution(contentString: String?): MetaParams? { + if (contentString == null) { + return null + } + val decodedContentString = try { + // content string can be in percent format + URLDecoder.decode(contentString, "UTF-8") + } catch (e: UnsupportedEncodingException) { + logger.error("failed to decode content string", e) + // can't recover from this + return null + } + + val data: String + val nonce: String + + val contentJson = try { + JSONObject(decodedContentString) + } catch (e: JSONException) { + logger.error("content is not JSON", e) + // can't recover from this + return null + } + + val app = try { + contentJson.optString(APP) ?: "" + } catch (e: JSONException) { + logger.error("failed to extract app", e) + // this is an acceptable outcome + "" + } + + val t = try { + contentJson.optString(T) ?: "" + } catch (e: JSONException) { + logger.error("failed to extract t", e) + // this is an acceptable outcome + "" + } + + try { + val source = contentJson.optJSONObject(SOURCE) + data = source?.optString(DATA) ?: "" + nonce = source?.optString(NONCE) ?: "" + } catch (e: JSONException) { + logger.error("failed to extract data or nonce", e) + // can't recover from this + return null + } + + if (data.isBlank() || nonce.isBlank()) { + return null + } + + return MetaParams( + app = app, + t = t, + data = data, + nonce = nonce, + ) + } + } + + /** + * record META attribution params to telemetry + */ + fun recordMetaAttribution() { + MetaAttribution.app.set(app) + MetaAttribution.t.set(t) + MetaAttribution.data.set(data) + MetaAttribution.nonce.set(nonce) } } diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserMenuSignIn.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserMenuSignIn.kt index 3f0872e84f..8cbd30bd43 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserMenuSignIn.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserMenuSignIn.kt @@ -21,7 +21,7 @@ import org.mozilla.fenix.ext.settings * A menu item for displaying account information. The item computes the label on every bind call, * to provide each menu with the latest account manager state. * - * @property textColorResource ID of color resource to tint the text. + * @param textColorResource ID of color resource to tint the text. * @param imageResource ID of a drawable resource to be shown as icon. * @param listener Callback to be invoked when this menu item is clicked. */ diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenter.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenter.kt index eac96fce2f..4065f87954 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenter.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenter.kt @@ -8,9 +8,17 @@ import android.content.Context import android.view.View import androidx.annotation.VisibleForTesting import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Icon import androidx.compose.material.Text +import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag @@ -34,8 +42,11 @@ import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.compose.cfr.CFRPopup import mozilla.components.compose.cfr.CFRPopup.PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR import mozilla.components.compose.cfr.CFRPopupProperties +import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingStatus import mozilla.components.lib.state.ext.flowScoped import mozilla.components.service.glean.private.NoExtras +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged +import org.mozilla.fenix.GleanMetrics.CookieBanners import org.mozilla.fenix.GleanMetrics.Shopping import org.mozilla.fenix.GleanMetrics.TrackingProtection import org.mozilla.fenix.R @@ -55,20 +66,22 @@ private const val CFR_TO_ANCHOR_VERTICAL_PADDING = -6 /** * The minimum number of opened tabs to show the Total Cookie Protection CFR. */ -private const val CFR_MINIMUM_NUMBER_OPENED_TABS = 5 +internal var CFR_MINIMUM_NUMBER_OPENED_TABS = 5 + @VisibleForTesting + internal set /** * Delegate for handling all the business logic for showing CFRs in the toolbar. * - * @property context used for various Android interactions. - * @property browserStore will be observed for tabs updates - * @property settings used to read and write persistent user settings - * @property toolbar will serve as anchor for the CFRs - * @property isPrivate Whether or not the session is private. - * @property sessionId optional custom tab id used to identify the custom tab in which to show a CFR. - * @property onShoppingCfrActionClicked Triggered when the user taps on the shopping CFR action. - * @property onShoppingCfrDisplayed Triggered when CFR is displayed to the user. - * @property shoppingExperienceFeature Used to determine if [ShoppingExperienceFeature] is enabled. + * @param context used for various Android interactions. + * @param browserStore will be observed for tabs updates + * @param settings used to read and write persistent user settings + * @param toolbar will serve as anchor for the CFRs + * @param isPrivate Whether or not the session is private. + * @param sessionId optional custom tab id used to identify the custom tab in which to show a CFR. + * @param onShoppingCfrActionClicked Triggered when the user taps on the shopping CFR action. + * @param onShoppingCfrDisplayed Triggered when CFR is displayed to the user. + * @param shoppingExperienceFeature Used to determine if [ShoppingExperienceFeature] is enabled. */ @Suppress("LongParameterList") class BrowserToolbarCFRPresenter( @@ -107,6 +120,24 @@ class BrowserToolbarCFRPresenter( } } } + ToolbarCFR.COOKIE_BANNERS -> { + scope = browserStore.flowScoped { flow -> + flow.mapNotNull { it.findCustomTabOrSelectedTab(sessionId) } + .ifAnyChanged { tab -> + arrayOf( + tab.cookieBanner, + ) + } + .filter { + it.content.private && it.cookieBanner == CookieBannerHandlingStatus.HANDLED + } + .collect { + scope?.cancel() + settings.shouldShowCookieBannersCFR = false + showCookieBannersCFR() + } + } + } ToolbarCFR.SHOPPING, ToolbarCFR.SHOPPING_OPTED_IN -> { scope = browserStore.flowScoped { flow -> @@ -185,10 +216,13 @@ class BrowserToolbarCFRPresenter( } settings.shouldShowTotalCookieProtectionCFR && ( - !settings.shouldShowCookieBannerReEngagementDialog() || - settings.openTabsCount >= CFR_MINIMUM_NUMBER_OPENED_TABS + settings.openTabsCount >= CFR_MINIMUM_NUMBER_OPENED_TABS ) -> ToolbarCFR.TCP + isPrivate && settings.shouldShowCookieBannersCFR && settings.shouldUseCookieBannerPrivateMode -> { + ToolbarCFR.COOKIE_BANNERS + } + shoppingExperienceFeature.isEnabled && settings.shouldShowReviewQualityCheckCFR -> whichShoppingCFR() @@ -324,6 +358,67 @@ class BrowserToolbarCFRPresenter( } } + @VisibleForTesting + @Suppress("LongMethod") + internal fun showCookieBannersCFR() { + CFRPopup( + anchor = toolbar.findViewById( + R.id.mozac_browser_toolbar_security_indicator, + ), + properties = CFRPopupProperties( + popupAlignment = INDICATOR_CENTERED_IN_ANCHOR, + popupBodyColors = listOf( + getColor(context, R.color.fx_mobile_layer_color_gradient_end), + getColor(context, R.color.fx_mobile_layer_color_gradient_start), + ), + popupVerticalOffset = CFR_TO_ANCHOR_VERTICAL_PADDING.dp, + dismissButtonColor = getColor(context, R.color.fx_mobile_icon_color_oncolor), + indicatorDirection = if (settings.toolbarPosition == ToolbarPosition.TOP) { + CFRPopup.IndicatorDirection.UP + } else { + CFRPopup.IndicatorDirection.DOWN + }, + ), + onDismiss = { + CookieBanners.cfrDismissal.record(NoExtras()) + }, + text = { + FirefoxTheme { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_cookies_disabled), + contentDescription = null, + tint = FirefoxTheme.colors.iconPrimary, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = context.getString( + R.string.cookie_banner_cfr_title, + context.getString(R.string.firefox), + ), + color = FirefoxTheme.colors.textOnColorPrimary, + style = FirefoxTheme.typography.subtitle2, + ) + } + Text( + text = context.getString(R.string.cookie_banner_cfr_message), + color = FirefoxTheme.colors.textOnColorPrimary, + style = FirefoxTheme.typography.body2, + modifier = Modifier.padding(top = 2.dp), + ) + } + } + }, + ).run { + popup = this + show() + CookieBanners.cfrShown.record(NoExtras()) + } + } + @VisibleForTesting internal fun showShoppingCFR(shouldShowOptedInCFR: Boolean) { CFRPopup( @@ -394,5 +489,5 @@ class BrowserToolbarCFRPresenter( * The CFR to be shown in the toolbar. */ private enum class ToolbarCFR { - TCP, SHOPPING, SHOPPING_OPTED_IN, ERASE, NONE + TCP, SHOPPING, SHOPPING_OPTED_IN, ERASE, COOKIE_BANNERS, NONE } diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt index de1cbf824e..3bb573b61e 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt @@ -94,6 +94,7 @@ class DefaultBrowserToolbarMenuController( override fun handleToolbarItemInteraction(item: ToolbarMenu.Item) { val sessionUseCases = activity.components.useCases.sessionUseCases val customTabUseCases = activity.components.useCases.customTabsUseCases + val tabsUseCases = activity.components.useCases.tabsUseCases trackToolbarItemInteraction(item) when (item) { @@ -254,6 +255,13 @@ class DefaultBrowserToolbarMenuController( ) } } + is ToolbarMenu.Item.OpenInRegularTab -> { + currentSession?.let { session -> + getProperUrl(session)?.let { url -> + tabsUseCases.migratePrivateTabUseCase.invoke(session.id, url) + } + } + } is ToolbarMenu.Item.AddToTopSites -> { scope.launch { val context = snackbarParent.context @@ -443,6 +451,8 @@ class DefaultBrowserToolbarMenuController( } else { Events.browserMenuAction.record(Events.BrowserMenuActionExtra("desktop_view_off")) } + is ToolbarMenu.Item.OpenInRegularTab -> + Events.browserMenuAction.record(Events.BrowserMenuActionExtra("open_in_regular_tab")) is ToolbarMenu.Item.FindInPage -> Events.browserMenuAction.record(Events.BrowserMenuActionExtra("find_in_page")) is ToolbarMenu.Item.SaveToCollection -> diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt index c209a2d30f..5085d54231 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt @@ -35,22 +35,24 @@ import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature import mozilla.components.lib.state.ext.flowScoped import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged +import org.mozilla.fenix.Config import org.mozilla.fenix.R import org.mozilla.fenix.components.accounts.FenixAccountManager import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.theme.ThemeManager +import org.mozilla.fenix.utils.Settings /** * Builds the toolbar object used with the 3-dot menu in the browser fragment. - * @property context a [Context] for accessing system resources. - * @property store reference to the application's [BrowserStore]. + * @param context a [Context] for accessing system resources. + * @param store reference to the application's [BrowserStore]. * @param hasAccountProblem If true, there was a problem signing into the Firefox account. - * @property onItemTapped Called when a menu item is tapped. - * @property lifecycleOwner View lifecycle owner used to determine when to cancel UI jobs. - * @property bookmarksStorage Used to check if a page is bookmarked. - * @property pinnedSiteStorage Used to check if the current url is a pinned site. + * @param onItemTapped Called when a menu item is tapped. + * @param lifecycleOwner View lifecycle owner used to determine when to cancel UI jobs. + * @param bookmarksStorage Used to check if a page is bookmarked. + * @param pinnedSiteStorage Used to check if the current url is a pinned site. * @property isPinningSupported true if the launcher supports adding shortcuts. */ @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") @@ -168,6 +170,19 @@ open class DefaultToolbarMenu( selectedSession != null && isPinningSupported && context.components.useCases.webAppUseCases.isInstallable() + /** + * Should the "Open in regular tab" menu item be visible? + */ + @VisibleForTesting(otherwise = PRIVATE) + fun shouldShowOpenInRegularTab(): Boolean = selectedSession?.let { session -> + // This feature is gated behind Nightly for the time being. + Config.channel.isNightlyOrDebug && + // This feature is explicitly for users opening links in private tabs. + context.settings().openLinksInAPrivateTab && + // and is only visible in private tabs. + session.content.private + } ?: false + @VisibleForTesting(otherwise = PRIVATE) fun shouldShowOpenInApp(): Boolean = selectedSession?.let { session -> val appLink = context.components.useCases.appLinksUseCases.appLinkRedirect @@ -182,7 +197,7 @@ open class DefaultToolbarMenu( private val installToHomescreen = BrowserMenuHighlightableItem( label = context.getString(R.string.browser_menu_install_on_homescreen), - startImageResource = R.drawable.mozac_ic_add_to_home_screen_24, + startImageResource = R.drawable.mozac_ic_add_to_homescreen_24, iconTintColorResource = primaryTextColor(), highlight = BrowserMenuHighlight.LowPriority( label = context.getString(R.string.browser_menu_install_on_homescreen), @@ -243,6 +258,13 @@ open class DefaultToolbarMenu( onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked)) } + private val openInRegularTabItem = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_open_in_regular_tab), + imageResource = R.drawable.ic_open_in_regular_tab, + ) { + onItemTapped.invoke(ToolbarMenu.Item.OpenInRegularTab) + } + private val customizeReaderView = BrowserMenuImageText( label = context.getString(R.string.browser_menu_customize_reader_view), imageResource = R.drawable.ic_readermode_appearance, @@ -271,7 +293,7 @@ open class DefaultToolbarMenu( private val addToHomeScreenItem = BrowserMenuImageText( label = context.getString(R.string.browser_menu_add_to_homescreen), - imageResource = R.drawable.mozac_ic_add_to_home_screen_24, + imageResource = R.drawable.mozac_ic_add_to_homescreen_24, iconTintColorResource = primaryTextColor(), isCollapsingMenuLimit = true, ) { @@ -384,6 +406,7 @@ open class DefaultToolbarMenu( BrowserMenuDivider(), findInPageItem, desktopSiteItem, + openInRegularTabItem.apply { visible = ::shouldShowOpenInRegularTab }, customizeReaderView.apply { visible = ::shouldShowReaderViewCustomization }, openInApp.apply { visible = ::shouldShowOpenInApp }, reportSiteIssuePlaceholder, diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt index a4f41ccdb2..120026d3c4 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt @@ -12,6 +12,11 @@ interface ToolbarMenu { sealed class Item { object Settings : Item() data class RequestDesktop(val isChecked: Boolean) : Item() + + /** + * Opens the current private tabs in a regular tab. + */ + object OpenInRegularTab : Item() object FindInPage : Item() object Share : Item() data class Back(val viewHistory: Boolean) : Item() diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/interactor/BrowserToolbarInteractor.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/interactor/BrowserToolbarInteractor.kt index be0c0b8e40..c309a85eb5 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/interactor/BrowserToolbarInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/interactor/BrowserToolbarInteractor.kt @@ -53,9 +53,9 @@ interface BrowserToolbarInteractor { /** * The default implementation of [BrowserToolbarInteractor]. * - * @property browserToolbarController [BrowserToolbarController] to which user actions can be + * @param browserToolbarController [BrowserToolbarController] to which user actions can be * delegated for all interactions on the browser toolbar. - * @property menuController [BrowserToolbarMenuController] to which user actions can be delegated + * @param menuController [BrowserToolbarMenuController] to which user actions can be delegated * for all interactions on the the browser toolbar menu. */ class DefaultBrowserToolbarInteractor( diff --git a/app/src/main/java/org/mozilla/fenix/compose/Favicon.kt b/app/src/main/java/org/mozilla/fenix/compose/Favicon.kt index eee49fb60c..e4031244ff 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/Favicon.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/Favicon.kt @@ -27,11 +27,12 @@ import org.mozilla.fenix.theme.FirefoxTheme /** * Load and display the favicon of a particular website. * - * @param url Website [url] for which the favicon will be shown. + * @param url Website URL for which the favicon will be shown. * @param size [Dp] height and width of the image to be loaded. * @param modifier [Modifier] to be applied to the layout. * @param isPrivate Whether or not a private request (like in private browsing) should be used to * download the icon (if needed). + * @param imageUrl Optional image URL to create an [IconRequest.Resource] from. */ @Composable fun Favicon( @@ -39,6 +40,7 @@ fun Favicon( size: Dp, modifier: Modifier = Modifier, isPrivate: Boolean = false, + imageUrl: String? = null, ) { if (inComposePreview) { FaviconPlaceholder( @@ -46,8 +48,16 @@ fun Favicon( modifier = modifier, ) } else { + val iconResource = imageUrl?.let { + IconRequest.Resource( + url = imageUrl, + type = IconRequest.Resource.Type.FAVICON, + ) + } + components.core.icons.LoadableImage( url = url, + iconResource = iconResource, isPrivate = isPrivate, iconSize = size.toIconRequestSize(), ) { diff --git a/app/src/main/java/org/mozilla/fenix/compose/button/Button.kt b/app/src/main/java/org/mozilla/fenix/compose/button/Button.kt index b2abda759e..bf0114ed8a 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/button/Button.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/button/Button.kt @@ -36,7 +36,10 @@ const val DEFAULT_MAX_LINES = 2 * @param textColor [Color] to apply to the button text. * @param backgroundColor The background [Color] of the button. * @param modifier [Modifier] to be applied to the layout. + * @param enabled Controls the enabled state of the button. + * When false, this button will not be clickable. * @param icon Optional [Painter] used to display a [Icon] before the button text. + * @param iconModifier [Modifier] to be applied to the icon. * @param tint Tint [Color] to be applied to the icon. * @param onClick Invoked when the user clicks on the button. */ @@ -46,7 +49,9 @@ private fun Button( textColor: Color, backgroundColor: Color, modifier: Modifier = Modifier, + enabled: Boolean = true, icon: Painter? = null, + iconModifier: Modifier = Modifier, tint: Color, onClick: () -> Unit, ) { @@ -56,6 +61,7 @@ private fun Button( androidx.compose.material.Button( onClick = onClick, modifier = modifier, + enabled = enabled, contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), elevation = ButtonDefaults.elevation(defaultElevation = 0.dp, pressedElevation = 0.dp), colors = ButtonDefaults.outlinedButtonColors( @@ -66,6 +72,7 @@ private fun Button( Icon( painter = painter, contentDescription = null, + modifier = iconModifier, tint = tint, ) @@ -87,18 +94,23 @@ private fun Button( * * @param text The button text to be displayed. * @param modifier [Modifier] to be applied to the layout. + * @param enabled Controls the enabled state of the button. + * When false, this button will not be clickable * @param textColor [Color] to apply to the button text. * @param backgroundColor The background [Color] of the button. * @param icon Optional [Painter] used to display an [Icon] before the button text. + * @param iconModifier [Modifier] to be applied to the icon. * @param onClick Invoked when the user clicks on the button. */ @Composable fun PrimaryButton( text: String, modifier: Modifier = Modifier.fillMaxWidth(), + enabled: Boolean = true, textColor: Color = FirefoxTheme.colors.textActionPrimary, backgroundColor: Color = FirefoxTheme.colors.actionPrimary, icon: Painter? = null, + iconModifier: Modifier = Modifier, onClick: () -> Unit, ) { Button( @@ -106,7 +118,9 @@ fun PrimaryButton( textColor = textColor, backgroundColor = backgroundColor, modifier = modifier, + enabled = enabled, icon = icon, + iconModifier = iconModifier, tint = FirefoxTheme.colors.iconActionPrimary, onClick = onClick, ) @@ -117,18 +131,23 @@ fun PrimaryButton( * * @param text The button text to be displayed. * @param modifier [Modifier] to be applied to the layout. + * @param enabled Controls the enabled state of the button. + * When false, this button will not be clickable * @param textColor [Color] to apply to the button text. * @param backgroundColor The background [Color] of the button. * @param icon Optional [Painter] used to display an [Icon] before the button text. + * @param iconModifier [Modifier] to be applied to the icon. * @param onClick Invoked when the user clicks on the button. */ @Composable fun SecondaryButton( text: String, modifier: Modifier = Modifier.fillMaxWidth(), + enabled: Boolean = true, textColor: Color = FirefoxTheme.colors.textActionSecondary, backgroundColor: Color = FirefoxTheme.colors.actionSecondary, icon: Painter? = null, + iconModifier: Modifier = Modifier, onClick: () -> Unit, ) { Button( @@ -136,7 +155,9 @@ fun SecondaryButton( textColor = textColor, backgroundColor = backgroundColor, modifier = modifier, + enabled = enabled, icon = icon, + iconModifier = iconModifier, tint = FirefoxTheme.colors.iconActionSecondary, onClick = onClick, ) @@ -147,18 +168,23 @@ fun SecondaryButton( * * @param text The button text to be displayed. * @param modifier [Modifier] to be applied to the layout. + * @param enabled Controls the enabled state of the button. + * When false, this button will not be clickable * @param textColor [Color] to apply to the button text. * @param backgroundColor The background [Color] of the button. * @param icon Optional [Painter] used to display an [Icon] before the button text. + * @param iconModifier [Modifier] to be applied to the icon. * @param onClick Invoked when the user clicks on the button. */ @Composable fun TertiaryButton( text: String, modifier: Modifier = Modifier.fillMaxWidth(), + enabled: Boolean = true, textColor: Color = FirefoxTheme.colors.textActionTertiary, backgroundColor: Color = FirefoxTheme.colors.actionTertiary, icon: Painter? = null, + iconModifier: Modifier = Modifier, onClick: () -> Unit, ) { Button( @@ -166,7 +192,9 @@ fun TertiaryButton( textColor = textColor, backgroundColor = backgroundColor, modifier = modifier, + enabled = enabled, icon = icon, + iconModifier = iconModifier, tint = FirefoxTheme.colors.iconActionTertiary, onClick = onClick, ) @@ -177,18 +205,23 @@ fun TertiaryButton( * * @param text The button text to be displayed. * @param modifier [Modifier] to be applied to the layout. + * @param enabled Controls the enabled state of the button. + * When false, this button will not be clickable * @param textColor [Color] to apply to the button text. * @param backgroundColor The background [Color] of the button. * @param icon Optional [Painter] used to display an [Icon] before the button text. + * @param iconModifier [Modifier] to be applied to the icon. * @param onClick Invoked when the user clicks on the button. */ @Composable fun DestructiveButton( text: String, modifier: Modifier = Modifier.fillMaxWidth(), + enabled: Boolean = true, textColor: Color = FirefoxTheme.colors.textWarningButton, backgroundColor: Color = FirefoxTheme.colors.actionSecondary, icon: Painter? = null, + iconModifier: Modifier = Modifier, onClick: () -> Unit, ) { Button( @@ -196,7 +229,9 @@ fun DestructiveButton( textColor = textColor, backgroundColor = backgroundColor, modifier = modifier, + enabled = enabled, icon = icon, + iconModifier = iconModifier, tint = FirefoxTheme.colors.iconWarningButton, onClick = onClick, ) diff --git a/app/src/main/java/org/mozilla/fenix/compose/ext/Modifier.kt b/app/src/main/java/org/mozilla/fenix/compose/ext/Modifier.kt index 4b3bd00797..a40001841a 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/ext/Modifier.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/ext/Modifier.kt @@ -4,8 +4,11 @@ package org.mozilla.fenix.compose.ext +import android.graphics.Rect import android.os.SystemClock +import androidx.annotation.FloatRange import androidx.compose.foundation.clickable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -17,7 +20,15 @@ import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize +import kotlinx.coroutines.delay +import kotlin.math.max +import kotlin.math.min /** * Add a dashed border around the current composable. @@ -100,3 +111,96 @@ fun Modifier.thenConditional( } else { this } + +/** + * The Composable this modifier is tied to may appear first and be fully constructed to then be pushed downwards + * when other elements appear. This can lead to over-counting impressions with multiple such events + * being possible without the user actually having time to see the UI or scrolling to it. + */ +private const val MINIMUM_TIME_TO_SETTLE_MS = 1000 + +/** + * Add a callback for when this Composable is "shown" on the screen. + * This checks whether the composable has at least [threshold] ratio of it's total area drawn inside + * the screen bounds. + * Does not account for other Views / Windows covering it. + * + * @param threshold The ratio of the total area to be within the screen bounds to trigger [onVisible]. + * @param settleTime The amount of time to wait before calling [onVisible]. + * @param onVisible Invoked when the UI is visible to the user. + * @param screenBounds Optional override to specify the exact bounds to detect the on-screen visibility. + */ +fun Modifier.onShown( + @FloatRange(from = 0.0, to = 1.0) threshold: Float, + settleTime: Int = MINIMUM_TIME_TO_SETTLE_MS, + onVisible: () -> Unit, + screenBounds: Rect? = null, +): Modifier { + val initialTime = System.currentTimeMillis() + var lastVisibleCoordinates: LayoutCoordinates? = null + + return composed { + var wasEventReported by remember { mutableStateOf(false) } + val bounds = screenBounds ?: Rect().apply { LocalView.current.getWindowVisibleDisplayFrame(this) } + + // In the event this composable starts as visible but then gets pushed offscreen + // before MINIMUM_TIME_TO_SETTLE_MS we will not report is as being visible. + // In the LaunchedEffect we add support for when the composable starts as visible and then + // it's position isn't changed after MINIMUM_TIME_TO_SETTLE_MS so it must be reported as visible. + LaunchedEffect(initialTime) { + delay(settleTime.toLong()) + if (!wasEventReported && lastVisibleCoordinates?.isVisible(bounds, threshold) == true) { + wasEventReported = true + onVisible() + } + } + + onGloballyPositioned { coordinates -> + if (!wasEventReported && coordinates.isVisible(bounds, threshold)) { + if (System.currentTimeMillis() - initialTime > settleTime) { + wasEventReported = true + onVisible() + } else { + lastVisibleCoordinates = coordinates + } + } + } + } +} + +/** + * Return whether this has at least [threshold] ratio of it's total area drawn inside + * the screen bounds. + */ +private fun LayoutCoordinates.isVisible( + visibleRect: Rect, + @FloatRange(from = 0.0, to = 1.0) threshold: Float, +): Boolean { + if (!isAttached) return false + + val boundsInWindow = boundsInWindow() + return Rect( + boundsInWindow.left.toInt(), + boundsInWindow.top.toInt(), + boundsInWindow.right.toInt(), + boundsInWindow.bottom.toInt(), + ).getIntersectPercentage(size, visibleRect) >= threshold +} + +/** + * Returns the ratio of how much this intersects with [other]. + * + * @param realSize [IntSize] containing the true height and width of the composable. + * @param other Other [Rect] for which to check the intersection area. + * + * @return A `0..1` float range for how much this [Rect] intersects with other. + */ +@FloatRange(from = 0.0, to = 1.0) +private fun Rect.getIntersectPercentage(realSize: IntSize, other: Rect): Float { + val composableArea = realSize.height * realSize.width + val heightOverlap = max(0, min(bottom, other.bottom) - max(top, other.top)) + val widthOverlap = max(0, min(right, other.right) - max(left, other.left)) + val intersectionArea = heightOverlap * widthOverlap + + return (intersectionArea.toFloat() / composableArea) +} diff --git a/app/src/main/java/org/mozilla/fenix/crashes/CrashContentIntegration.kt b/app/src/main/java/org/mozilla/fenix/crashes/CrashContentIntegration.kt index d95b06e6f3..c866a04767 100644 --- a/app/src/main/java/org/mozilla/fenix/crashes/CrashContentIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/crashes/CrashContentIntegration.kt @@ -24,17 +24,17 @@ import org.mozilla.fenix.utils.Settings /** * Helper for observing [BrowserStore] and show an in-app crash reporter for tabs with content crashes. * - * @property browserStore [BrowserStore] observed for any changes related to [EngineState.crashed]. - * @property appStore [AppStore] that tracks all content crashes in the current app session until the user + * @param browserStore [BrowserStore] observed for any changes related to [EngineState.crashed]. + * @param appStore [AppStore] that tracks all content crashes in the current app session until the user * decides to either send or dismiss all crash reports. - * @property toolbar [BrowserToolbar] that will be expanded when showing the in-app crash reporter. - * @property isToolbarPlacedAtTop [Boolean] based allowing the in-app crash reporter to be shown as + * @param toolbar [BrowserToolbar] that will be expanded when showing the in-app crash reporter. + * @param isToolbarPlacedAtTop [Boolean] based allowing the in-app crash reporter to be shown as * immediately below or above the toolbar. - * @property crashReporterView [CrashContentView] which will be shown if the current tab is marked as crashed. - * @property components [Components] allowing interactions with other app features. - * @property settings [Settings] allowing to check whether crash reporting is enabled or not. - * @property navController [NavController] used to navigate to other parts of the app. - * @property sessionId [String] Id of the tab or custom tab which should be observed for [EngineState.crashed] + * @param crashReporterView [CrashContentView] which will be shown if the current tab is marked as crashed. + * @param components [Components] allowing interactions with other app features. + * @param settings [Settings] allowing to check whether crash reporting is enabled or not. + * @param navController [NavController] used to navigate to other parts of the app. + * @param sessionId [String] Id of the tab or custom tab which should be observed for [EngineState.crashed] * depending on which [crashReporterView] will be shown or hidden. */ diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt index 7863adc050..9a0834b133 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt @@ -32,12 +32,12 @@ import java.util.Locale /** * Builds the toolbar object used with the 3-dot menu in the custom tab browser fragment. * - * @property context An Android [Context]. - * @property store reference to the application's [BrowserStore]. - * @property sessionId ID of the open custom tab session. - * @property shouldReverseItems If true, reverse the menu items. - * @property isSandboxCustomTab If true, menu should not show the "Open in Firefox" and "POWERED BY FIREFOX" items. - * @property onItemTapped Called when a menu item is tapped. + * @param context An Android [Context]. + * @param store reference to the application's [BrowserStore]. + * @param sessionId ID of the open custom tab session. + * @param shouldReverseItems If true, reverse the menu items. + * @param isSandboxCustomTab If true, menu should not show the "Open in Firefox" and "POWERED BY FIREFOX" items. + * @param onItemTapped Called when a menu item is tapped. */ class CustomTabToolbarMenu( private val context: Context, diff --git a/app/src/main/java/org/mozilla/fenix/downloads/StartDownloadDialog.kt b/app/src/main/java/org/mozilla/fenix/downloads/StartDownloadDialog.kt index 9c9f17155b..6ed2c57fb5 100644 --- a/app/src/main/java/org/mozilla/fenix/downloads/StartDownloadDialog.kt +++ b/app/src/main/java/org/mozilla/fenix/downloads/StartDownloadDialog.kt @@ -34,7 +34,7 @@ import org.mozilla.fenix.ext.settings /** * Parent of all download views that can mimic a modal [Dialog]. * - * @property activity The [Activity] in which the dialog will be shown. + * @param activity The [Activity] in which the dialog will be shown. * Used to update the activity [Window] to best mimic a modal dialog. */ abstract class StartDownloadDialog( @@ -153,13 +153,13 @@ abstract class StartDownloadDialog( /** * A download view mimicking a modal dialog that allows the user to download a file with the current application. * - * @property activity The [Activity] in which the dialog will be shown. + * @param activity The [Activity] in which the dialog will be shown. * Used to update the activity [Window] to best mimic a modal dialog. - * @property filename Name of the file to be downloaded. It wil be shown without any modification. - * @property contentSize Size of the file to be downloaded expressed as a number of bytes. + * @param filename Name of the file to be downloaded. It wil be shown without any modification. + * @param contentSize Size of the file to be downloaded expressed as a number of bytes. * It will automatically be parsed to the appropriate kilobyte or megabyte value before being shown. - * @property positiveButtonAction Callback for when the user interacts with the dialog to start the download. - * @property negativeButtonAction Callback for when the user interacts with the dialog to dismiss it. + * @param positiveButtonAction Callback for when the user interacts with the dialog to start the download. + * @param negativeButtonAction Callback for when the user interacts with the dialog to dismiss it. */ class FirstPartyDownloadDialog( private val activity: Activity, @@ -212,11 +212,11 @@ class FirstPartyDownloadDialog( * A download view mimicking a modal dialog that presents the user with a list of all apps * that can handle the download request. * - * @property activity The [Activity] in which the dialog will be shown. + * @param activity The [Activity] in which the dialog will be shown. * Used to update the activity [Window] to best mimic a modal dialog. - * @property downloaderApps List of all applications that can handle the download request. - * @property onAppSelected Callback for when the user chooses a specific application to handle the download request. - * @property negativeButtonAction Callback for when the user interacts with the dialog to dismiss it. + * @param downloaderApps List of all applications that can handle the download request. + * @param onAppSelected Callback for when the user chooses a specific application to handle the download request. + * @param negativeButtonAction Callback for when the user interacts with the dialog to dismiss it. */ class ThirdPartyDownloadDialog( private val activity: Activity, diff --git a/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.kt b/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.kt index a9b7f67452..f6ecca1cfd 100644 --- a/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.kt +++ b/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.kt @@ -64,6 +64,7 @@ fun createNimbus(context: Context, urlString: String?): NimbusApi { initialExperiments = R.raw.initial_experiments timeoutLoadingExperiment = TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS usePreviewCollection = context.settings().nimbusUsePreview + sharedPreferences = context.settings().preferences isFirstRun = isAppFirstRun featureManifest = FxNimbus onFetchCallback = { diff --git a/app/src/main/java/org/mozilla/fenix/gecko/GeckoProvider.kt b/app/src/main/java/org/mozilla/fenix/gecko/GeckoProvider.kt index 740b1f79b2..eaeba9b040 100644 --- a/app/src/main/java/org/mozilla/fenix/gecko/GeckoProvider.kt +++ b/app/src/main/java/org/mozilla/fenix/gecko/GeckoProvider.kt @@ -110,7 +110,27 @@ object GeckoProvider { .crashHandler(CrashHandlerService::class.java) .telemetryDelegate(GeckoAdapter()) .experimentDelegate(NimbusExperimentDelegate()) - .contentBlocking(policy.toContentBlockingSetting()) + .contentBlocking( + policy.toContentBlockingSetting( + cookieBannerHandlingMode = context.settings().getCookieBannerHandling(), + cookieBannerHandlingModePrivateBrowsing = context.settings() + .getCookieBannerHandlingPrivateMode(), + cookieBannerHandlingDetectOnlyMode = + context.settings().shouldEnableCookieBannerDetectOnly, + cookieBannerGlobalRulesEnabled = + context.settings().shouldEnableCookieBannerGlobalRules, + cookieBannerGlobalRulesSubFramesEnabled = + context.settings().shouldEnableCookieBannerGlobalRulesSubFrame, + queryParameterStripping = + context.settings().shouldEnableQueryParameterStripping, + queryParameterStrippingPrivateBrowsing = + context.settings().shouldEnableQueryParameterStrippingPrivateBrowsing, + queryParameterStrippingAllowList = + context.settings().queryParameterStrippingAllowList, + queryParameterStrippingStripList = + context.settings().queryParameterStrippingStripList, + ), + ) .consoleOutput(context.components.settings.enableGeckoLogs) .debugLogging(Config.channel.isDebug || context.components.settings.enableGeckoLogs) .aboutConfigEnabled(true) diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index 7dc8e40024..30904f087a 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -249,7 +249,7 @@ class HomeFragment : Fragment() { val currentWallpaperName = requireContext().settings().currentWallpaperName applyWallpaper(wallpaperName = currentWallpaperName, orientationChange = false) - components.appStore.dispatch(AppAction.ModeChange(Mode.fromBrowsingMode(browsingModeManager.mode))) + components.appStore.dispatch(AppAction.ModeChange(browsingModeManager.mode)) lifecycleScope.launch(IO) { if (requireContext().settings().showPocketRecommendationsFeature) { diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeMenuView.kt b/app/src/main/java/org/mozilla/fenix/home/HomeMenuView.kt index 836819470a..81c28352ec 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeMenuView.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeMenuView.kt @@ -12,6 +12,8 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController +import mozilla.appservices.fxaclient.FxaServer +import mozilla.appservices.fxaclient.contentUrl import mozilla.appservices.places.BookmarkRoot import mozilla.components.browser.menu.view.MenuButton import mozilla.components.concept.sync.FxAEntryPoint @@ -36,14 +38,14 @@ import org.mozilla.fenix.GleanMetrics.HomeMenu as HomeMenuMetrics /** * Helper class for building the [HomeMenu]. * - * @property view The [View] to attach the snackbar to. - * @property context An Android [Context]. - * @property lifecycleOwner [LifecycleOwner] for the view. - * @property homeActivity [HomeActivity] used to open URLs in a new tab. - * @property navController [NavController] used for navigation. - * @property menuButton The [MenuButton] that will be used to create a menu when the button is + * @param view The [View] to attach the snackbar to. + * @param context An Android [Context]. + * @param lifecycleOwner [LifecycleOwner] for the view. + * @param homeActivity [HomeActivity] used to open URLs in a new tab. + * @param navController [NavController] used for navigation. + * @param menuButton The [MenuButton] that will be used to create a menu when the button is * clicked. - * @property fxaEntrypoint The source entry point to FxA. + * @param fxaEntrypoint The source entry point to FxA. */ @Suppress("LongParameterList") class HomeMenuView( @@ -127,9 +129,9 @@ class HomeMenuView( homeActivity.openToBrowserAndLoad( searchTermOrURL = if (context.settings().allowDomesticChinaFxaServer) { - mozilla.appservices.fxaclient.Config.Server.CHINA.contentUrl + "/settings" + FxaServer.China.contentUrl() + "/settings" } else { - mozilla.appservices.fxaclient.Config.Server.RELEASE.contentUrl + "/settings" + FxaServer.Release.contentUrl() + "/settings" }, newTab = true, from = BrowserDirection.FromHome, diff --git a/app/src/main/java/org/mozilla/fenix/home/Mode.kt b/app/src/main/java/org/mozilla/fenix/home/Mode.kt deleted file mode 100644 index 4674b0ff77..0000000000 --- a/app/src/main/java/org/mozilla/fenix/home/Mode.kt +++ /dev/null @@ -1,22 +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.home - -import org.mozilla.fenix.browser.browsingmode.BrowsingMode - -/** - * Describes various states of the home fragment UI. - */ -sealed class Mode { - object Normal : Mode() - object Private : Mode() - - companion object { - fun fromBrowsingMode(browsingMode: BrowsingMode) = when (browsingMode) { - BrowsingMode.Normal -> Normal - BrowsingMode.Private -> Private - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/home/PocketUpdatesMiddleware.kt b/app/src/main/java/org/mozilla/fenix/home/PocketUpdatesMiddleware.kt index 3b7e501134..755ec4e82f 100644 --- a/app/src/main/java/org/mozilla/fenix/home/PocketUpdatesMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/home/PocketUpdatesMiddleware.kt @@ -29,10 +29,10 @@ import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory /** * [AppStore] middleware reacting in response to Pocket related [Action]s. * - * @property pocketStoriesService [PocketStoriesService] used for updating details about the Pocket recommended stories. - * @property selectedPocketCategoriesDataStore [DataStore] used for reading or persisting details about the + * @param pocketStoriesService [PocketStoriesService] used for updating details about the Pocket recommended stories. + * @param selectedPocketCategoriesDataStore [DataStore] used for reading or persisting details about the * currently selected Pocket recommended stories categories. - * @property coroutineScope [CoroutineScope] used for long running operations like disk IO. + * @param coroutineScope [CoroutineScope] used for long running operations like disk IO. */ class PocketUpdatesMiddleware( private val pocketStoriesService: PocketStoriesService, diff --git a/app/src/main/java/org/mozilla/fenix/home/TabCounterView.kt b/app/src/main/java/org/mozilla/fenix/home/TabCounterView.kt index a5eb2e49bd..d9bd97a7d9 100644 --- a/app/src/main/java/org/mozilla/fenix/home/TabCounterView.kt +++ b/app/src/main/java/org/mozilla/fenix/home/TabCounterView.kt @@ -26,10 +26,10 @@ import org.mozilla.fenix.tabstray.Page /** * Helper class for building the [FenixTabCounterMenu]. * - * @property context An Android [Context]. - * @property browsingModeManager [BrowsingModeManager] used for fetching the current browsing mode. - * @property navController [NavController] used for navigation. - * @property tabCounter The [TabCounter] that will be setup with event handlers. + * @param context An Android [Context]. + * @param browsingModeManager [BrowsingModeManager] used for fetching the current browsing mode. + * @param navController [NavController] used for navigation. + * @param tabCounter The [TabCounter] that will be setup with event handlers. */ class TabCounterView( private val context: Context, diff --git a/app/src/main/java/org/mozilla/fenix/home/blocklist/BlocklistMiddleware.kt b/app/src/main/java/org/mozilla/fenix/home/blocklist/BlocklistMiddleware.kt index 56a5e5709e..9c8928c41d 100644 --- a/app/src/main/java/org/mozilla/fenix/home/blocklist/BlocklistMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/home/blocklist/BlocklistMiddleware.kt @@ -14,7 +14,7 @@ import org.mozilla.fenix.home.recenttabs.RecentTab * This [Middleware] reacts to item removals from the home screen, adding them to a blocklist. * Additionally, it reacts to state changes in order to filter them by the blocklist. * - * @property blocklistHandler An instance of [BlocklistHandler] for interacting with the blocklist + * @param blocklistHandler An instance of [BlocklistHandler] for interacting with the blocklist * stored in shared preferences. */ class BlocklistMiddleware( diff --git a/app/src/main/java/org/mozilla/fenix/home/collections/CollectionViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/collections/CollectionViewHolder.kt index 0846eabebc..cd70ec0612 100644 --- a/app/src/main/java/org/mozilla/fenix/home/collections/CollectionViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/collections/CollectionViewHolder.kt @@ -36,7 +36,7 @@ import org.mozilla.fenix.theme.FirefoxTheme * * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. * @param viewLifecycleOwner [LifecycleOwner] to which this Composable will be tied to. - * @property interactor [CollectionInteractor] callback for user interactions. + * @param interactor [CollectionInteractor] callback for user interactions. */ class CollectionViewHolder( composeView: ComposeView, diff --git a/app/src/main/java/org/mozilla/fenix/home/collections/TabInCollectionViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/collections/TabInCollectionViewHolder.kt index c14ebd0e49..fa3d12967d 100644 --- a/app/src/main/java/org/mozilla/fenix/home/collections/TabInCollectionViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/collections/TabInCollectionViewHolder.kt @@ -25,7 +25,7 @@ import org.mozilla.fenix.home.sessioncontrol.CollectionInteractor * * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. * @param viewLifecycleOwner [LifecycleOwner] to which this Composable will be tied to. - * @property interactor [CollectionInteractor] callback for user interactions. + * @param interactor [CollectionInteractor] callback for user interactions. */ class TabInCollectionViewHolder( composeView: ComposeView, diff --git a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketCategoriesViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketCategoriesViewHolder.kt index b6b895990a..7ca7b47d6a 100644 --- a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketCategoriesViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketCategoriesViewHolder.kt @@ -35,7 +35,7 @@ internal const val POCKET_CATEGORIES_SELECTED_AT_A_TIME_COUNT = 8 * * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. * @param viewLifecycleOwner [LifecycleOwner] to which this Composable will be tied to. - * @property interactor [PocketStoriesInteractor] callback for user interaction. + * @param interactor [PocketStoriesInteractor] callback for user interaction. */ class PocketCategoriesViewHolder( composeView: ComposeView, diff --git a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketRecommendationsHeaderViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketRecommendationsHeaderViewHolder.kt index 1ee2552a63..43343622b4 100644 --- a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketRecommendationsHeaderViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketRecommendationsHeaderViewHolder.kt @@ -32,7 +32,7 @@ import org.mozilla.fenix.theme.FirefoxTheme * * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. * @param viewLifecycleOwner [LifecycleOwner] to which this Composable will be tied to. - * @property interactor [PocketStoriesInteractor] callback for user interaction. + * @param interactor [PocketStoriesInteractor] callback for user interaction. */ class PocketRecommendationsHeaderViewHolder( composeView: ComposeView, diff --git a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesComposables.kt b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesComposables.kt index 39aef4981a..3b224dc82d 100644 --- a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesComposables.kt +++ b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesComposables.kt @@ -9,7 +9,6 @@ package org.mozilla.fenix.home.pocket import android.content.res.Configuration import android.graphics.Rect import android.net.Uri -import androidx.annotation.FloatRange import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -27,19 +26,12 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.LayoutCoordinates -import androidx.compose.ui.layout.boundsInWindow -import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -58,10 +50,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max -import kotlinx.coroutines.delay import mozilla.components.service.pocket.PocketStory import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory @@ -78,25 +68,16 @@ import org.mozilla.fenix.compose.SelectableChip import org.mozilla.fenix.compose.SelectableChipColors import org.mozilla.fenix.compose.StaggeredHorizontalGrid import org.mozilla.fenix.compose.TabSubtitleWithInterdot +import org.mozilla.fenix.compose.ext.onShown import org.mozilla.fenix.compose.inComposePreview import org.mozilla.fenix.ext.settings import org.mozilla.fenix.theme.FirefoxTheme -import kotlin.math.max -import kotlin.math.min import kotlin.math.roundToInt private const val URI_PARAM_UTM_KEY = "utm_source" private const val POCKET_STORIES_UTM_VALUE = "pocket-newtab-android" private const val POCKET_FEATURE_UTM_KEY_VALUE = "utm_source=ff_android" -/** - * The Pocket section may appear first on the homescreen and be fully constructed - * to then be pushed downwards when other elements appear. - * This can lead to overcounting impressions with multiple such events being possible - * without the user actually having time to see the stories or scrolling to see the Pocket section. - */ -private const val MINIMUM_TIME_TO_SETTLE_MS = 1000 - /** * Placeholder [PocketStory] allowing to combine other items in the same list that shows stories. * It uses empty values for it's properties ensuring that no conflict is possible since real stories have @@ -258,7 +239,7 @@ fun PocketSponsoredStory( * @param onDiscoverMoreClicked Callback for when the user taps an element which contains an */ @OptIn(ExperimentalComposeUiApi::class) -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LongMethod") @Composable fun PocketStories( @PreviewParameter(PocketStoryProvider::class) stories: List, @@ -325,10 +306,28 @@ fun PocketStories( onStoryClicked(it.copy(url = uri), rowIndex to columnIndex) } } else if (story is PocketSponsoredStory) { + val screenBounds = Rect() + .apply { LocalView.current.getWindowVisibleDisplayFrame(this) } + .apply { + // Check if this is in a preview because `.settings()` breaks previews + if (!inComposePreview) { + val verticalOffset = LocalContext.current.resources.getDimensionPixelSize( + R.dimen.browser_toolbar_height, + ) + + if (LocalContext.current.settings().shouldUseBottomToolbar) { + bottom -= verticalOffset + } else { + top += verticalOffset + } + } + } Box( - modifier = Modifier.onShown(0.5f) { - onStoryShown(story, rowIndex to columnIndex) - }, + modifier = Modifier.onShown( + threshold = 0.5f, + onVisible = { onStoryShown(story, rowIndex to columnIndex) }, + screenBounds = screenBounds, + ), ) { PocketSponsoredStory( story = story, @@ -358,101 +357,6 @@ private fun endPadding(configuration: Configuration, screenWidth: Dp, contentPad private fun alignColumnToTitlePadding(screenWidth: Dp, contentPadding: Dp) = max(screenWidth - (ITEM_WIDTH.dp + contentPadding), contentPadding) -/** - * Add a callback for when this Composable is "shown" on the screen. - * This checks whether the composable has at least [threshold] ratio of it's total area drawn inside - * the screen bounds. - * Does not account for other Views / Windows covering it. - */ -private fun Modifier.onShown( - @FloatRange(from = 0.0, to = 1.0) threshold: Float, - onVisible: () -> Unit, -): Modifier { - val initialTime = System.currentTimeMillis() - var lastVisibleCoordinates: LayoutCoordinates? = null - - return composed { - if (inComposePreview) { - Modifier - } else { - val context = LocalContext.current - var wasEventReported by remember { mutableStateOf(false) } - - val toolbarHeight = context.resources.getDimensionPixelSize(R.dimen.browser_toolbar_height) - val isToolbarPlacedAtBottom = context.settings().shouldUseBottomToolbar - // Get a Rect of the entire screen minus system insets minus the toolbar - val screenBounds = Rect() - .apply { LocalView.current.getWindowVisibleDisplayFrame(this) } - .apply { - when (isToolbarPlacedAtBottom) { - true -> bottom -= toolbarHeight - false -> top += toolbarHeight - } - } - - // In the event this composable starts as visible but then gets pushed offscreen - // before MINIMUM_TIME_TO_SETTLE_MS we will not report is as being visible. - // In the LaunchedEffect we add support for when the composable starts as visible and then - // it's position isn't changed after MINIMUM_TIME_TO_SETTLE_MS so it must be reported as visible. - LaunchedEffect(initialTime) { - delay(MINIMUM_TIME_TO_SETTLE_MS.toLong()) - if (!wasEventReported && lastVisibleCoordinates?.isVisible(screenBounds, threshold) == true) { - wasEventReported = true - onVisible() - } - } - - onGloballyPositioned { coordinates -> - if (!wasEventReported && coordinates.isVisible(screenBounds, threshold)) { - if (System.currentTimeMillis() - initialTime > MINIMUM_TIME_TO_SETTLE_MS) { - wasEventReported = true - onVisible() - } else { - lastVisibleCoordinates = coordinates - } - } - } - } - } -} - -/** - * Return whether this has at least [threshold] ratio of it's total area drawn inside - * the screen bounds. - */ -private fun LayoutCoordinates.isVisible( - visibleRect: Rect, - @FloatRange(from = 0.0, to = 1.0) threshold: Float, -): Boolean { - if (!isAttached) return false - - val boundsInWindow = boundsInWindow() - return Rect( - boundsInWindow.left.toInt(), - boundsInWindow.top.toInt(), - boundsInWindow.right.toInt(), - boundsInWindow.bottom.toInt(), - ).getIntersectPercentage(size, visibleRect) >= threshold -} - -/** - * Returns the ratio of how much this intersects with [other]. - * - * @param realSize [IntSize] containing the true height and width of the composable. - * @param other Other [Rect] for whcih to check the intersection area. - * - * @return A `0..1` float range for how much this [Rect] intersects with other. - */ -@FloatRange(from = 0.0, to = 1.0) -private fun Rect.getIntersectPercentage(realSize: IntSize, other: Rect): Float { - val composableArea = realSize.height * realSize.width - val heightOverlap = max(0, min(bottom, other.bottom) - max(top, other.top)) - val widthOverlap = max(0, min(right, other.right) - max(left, other.left)) - val intersectionArea = heightOverlap * widthOverlap - - return (intersectionArea.toFloat() / composableArea) -} - /** * Displays a list of [PocketRecommendedStoriesCategory]s. * diff --git a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesController.kt b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesController.kt index 60e0099ae7..e877ab455e 100644 --- a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesController.kt +++ b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesController.kt @@ -68,8 +68,8 @@ interface PocketStoriesController { /** * Default behavior for handling all user interactions with the Pocket recommended stories feature. * - * @property homeActivity [HomeActivity] used to open URLs in a new tab. - * @property appStore [AppStore] from which to read the current Pocket recommendations and dispatch new actions on. + * @param homeActivity [HomeActivity] used to open URLs in a new tab. + * @param appStore [AppStore] from which to read the current Pocket recommendations and dispatch new actions on. */ internal class DefaultPocketStoriesController( private val homeActivity: HomeActivity, diff --git a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesViewHolder.kt index bbc0595e51..760420ed5f 100644 --- a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesViewHolder.kt @@ -34,7 +34,7 @@ import org.mozilla.fenix.wallpapers.WallpaperState * * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. * @param viewLifecycleOwner [LifecycleOwner] to which this Composable will be tied to. - * @property interactor [PocketStoriesInteractor] callback for user interaction. + * @param interactor [PocketStoriesInteractor] callback for user interaction. */ class PocketStoriesViewHolder( composeView: ComposeView, diff --git a/app/src/main/java/org/mozilla/fenix/home/privatebrowsing/controller/PrivateBrowsingController.kt b/app/src/main/java/org/mozilla/fenix/home/privatebrowsing/controller/PrivateBrowsingController.kt index e0a7a1f64f..56cd4d419a 100644 --- a/app/src/main/java/org/mozilla/fenix/home/privatebrowsing/controller/PrivateBrowsingController.kt +++ b/app/src/main/java/org/mozilla/fenix/home/privatebrowsing/controller/PrivateBrowsingController.kt @@ -13,7 +13,6 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.ext.settings -import org.mozilla.fenix.home.Mode import org.mozilla.fenix.home.privatebrowsing.interactor.PrivateBrowsingInteractor import org.mozilla.fenix.settings.SupportUtils @@ -58,7 +57,7 @@ class DefaultPrivateBrowsingController( } appStore.dispatch( - AppAction.ModeChange(Mode.fromBrowsingMode(newMode)), + AppAction.ModeChange(newMode), ) if (navController.currentDestination?.id == R.id.searchDialogFragment) { diff --git a/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/RecentBookmarksFeature.kt b/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/RecentBookmarksFeature.kt index 146ab03cb4..3e7dc9d6c5 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/RecentBookmarksFeature.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/RecentBookmarksFeature.kt @@ -20,11 +20,11 @@ import org.mozilla.fenix.home.HomeFragment * View-bound feature that retrieves a list of recently added [BookmarkNode]s and dispatches * updates to the [AppStore]. * - * @property appStore the [AppStore] that holds the state of the [HomeFragment]. - * @property bookmarksUseCase the [BookmarksUseCase] for retrieving the list of recently saved + * @param appStore the [AppStore] that holds the state of the [HomeFragment]. + * @param bookmarksUseCase the [BookmarksUseCase] for retrieving the list of recently saved * bookmarks from storage. - * @property scope the [CoroutineScope] used to fetch the bookmarks list - * @property ioDispatcher the [CoroutineDispatcher] for performing read/write operations. + * @param scope the [CoroutineScope] used to fetch the bookmarks list + * @param ioDispatcher the [CoroutineDispatcher] for performing read/write operations. */ class RecentBookmarksFeature( private val appStore: AppStore, diff --git a/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarksHeaderViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarksHeaderViewHolder.kt index dcc7dbad18..210bad9623 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarksHeaderViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarksHeaderViewHolder.kt @@ -24,7 +24,7 @@ import org.mozilla.fenix.home.recentbookmarks.interactor.RecentBookmarksInteract * * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. * @param viewLifecycleOwner [LifecycleOwner] life cycle owner for the view. - * @property interactor [RecentBookmarksInteractor] which will have delegated to all user interactions. + * @param interactor [RecentBookmarksInteractor] which will have delegated to all user interactions. */ class RecentBookmarksHeaderViewHolder( composeView: ComposeView, diff --git a/app/src/main/java/org/mozilla/fenix/home/recentsyncedtabs/RecentSyncedTabFeature.kt b/app/src/main/java/org/mozilla/fenix/home/recentsyncedtabs/RecentSyncedTabFeature.kt index 38bab9d3c7..ec33d98a55 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recentsyncedtabs/RecentSyncedTabFeature.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recentsyncedtabs/RecentSyncedTabFeature.kt @@ -33,13 +33,13 @@ import java.util.concurrent.TimeUnit /** * Delegate to handle layout updates and dispatch actions related to the recent synced tab. * - * @property context An Android [Context]. - * @property appStore Store to dispatch actions to when synced tabs are updated or errors encountered. - * @property syncStore Store to observe for changes to Sync and account status. - * @property storage Storage layer for synced tabs. - * @property accountManager Account manager to initiate Syncs and refresh devices. - * @property historyStorage Storage for searching history for preview image URLs matching synced tab. - * @property coroutineScope The scope to collect Sync state Flow updates in. + * @param context An Android [Context]. + * @param appStore Store to dispatch actions to when synced tabs are updated or errors encountered. + * @param syncStore Store to observe for changes to Sync and account status. + * @param storage Storage layer for synced tabs. + * @param accountManager Account manager to initiate Syncs and refresh devices. + * @param historyStorage Storage for searching history for preview image URLs matching synced tab. + * @param coroutineScope The scope to collect Sync state Flow updates in. */ @Suppress("LongParameterList") class RecentSyncedTabFeature( diff --git a/app/src/main/java/org/mozilla/fenix/home/recentsyncedtabs/controller/RecentSyncedTabController.kt b/app/src/main/java/org/mozilla/fenix/home/recentsyncedtabs/controller/RecentSyncedTabController.kt index ff23ae2836..5888a59df7 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recentsyncedtabs/controller/RecentSyncedTabController.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recentsyncedtabs/controller/RecentSyncedTabController.kt @@ -42,10 +42,10 @@ interface RecentSyncedTabController { /** * The default implementation of [RecentSyncedTabController]. * - * @property tabsUseCase Use cases to open the synced tab when clicked. - * @property navController [NavController] to navigate to synced tabs tray. - * @property accessPoint The action or screen that was used to navigate to the tabs tray. - * @property appStore The [AppStore] that holds the state of the [HomeFragment]. + * @param tabsUseCase Use cases to open the synced tab when clicked. + * @param navController [NavController] to navigate to synced tabs tray. + * @param accessPoint The action or screen that was used to navigate to the tabs tray. + * @param appStore The [AppStore] that holds the state of the [HomeFragment]. */ class DefaultRecentSyncedTabController( private val tabsUseCase: TabsUseCases, diff --git a/app/src/main/java/org/mozilla/fenix/home/recentsyncedtabs/view/RecentSyncedTabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/recentsyncedtabs/view/RecentSyncedTabViewHolder.kt index c5707e48d8..c943c637b7 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recentsyncedtabs/view/RecentSyncedTabViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recentsyncedtabs/view/RecentSyncedTabViewHolder.kt @@ -24,7 +24,7 @@ import org.mozilla.fenix.wallpapers.WallpaperState * * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. * @param viewLifecycleOwner [LifecycleOwner] to which this Composable will be tied to. - * @property recentSyncedTabInteractor [RecentSyncedTabInteractor] which will have delegated to all + * @param recentSyncedTabInteractor [RecentSyncedTabInteractor] which will have delegated to all * recent synced tab user interactions. */ class RecentSyncedTabViewHolder( diff --git a/app/src/main/java/org/mozilla/fenix/home/recenttabs/controller/RecentTabController.kt b/app/src/main/java/org/mozilla/fenix/home/recenttabs/controller/RecentTabController.kt index 20ba217cda..ae052e1c04 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recenttabs/controller/RecentTabController.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recenttabs/controller/RecentTabController.kt @@ -40,9 +40,9 @@ interface RecentTabController { /** * The default implementation of [RecentTabController]. * - * @property selectTabUseCase [SelectTabUseCase] used selecting a tab. - * @property navController [NavController] used for navigation. - * @property appStore The [AppStore] that holds the state of the [HomeFragment]. + * @param selectTabUseCase [SelectTabUseCase] used selecting a tab. + * @param navController [NavController] used for navigation. + * @param appStore The [AppStore] that holds the state of the [HomeFragment]. */ class DefaultRecentTabsController( private val selectTabUseCase: SelectTabUseCase, diff --git a/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewHolder.kt index 6426fb4393..c745cd96e9 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewHolder.kt @@ -21,7 +21,7 @@ import org.mozilla.fenix.wallpapers.WallpaperState * * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. * @param viewLifecycleOwner [LifecycleOwner] to which this Composable will be tied to. - * @property recentTabInteractor [RecentTabInteractor] which will have delegated to all user recent + * @param recentTabInteractor [RecentTabInteractor] which will have delegated to all user recent * tab interactions. */ class RecentTabViewHolder( diff --git a/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabsHeaderViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabsHeaderViewHolder.kt index 80f4d2fc27..fe6038ef9c 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabsHeaderViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabsHeaderViewHolder.kt @@ -24,7 +24,7 @@ import org.mozilla.fenix.home.recenttabs.interactor.RecentTabInteractor * * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. * @param viewLifecycleOwner [LifecycleOwner] to which this Composable will be tied to. - * @property interactor [RecentTabInteractor] which will have delegated to all user interactions. + * @param interactor [RecentTabInteractor] which will have delegated to all user interactions. */ class RecentTabsHeaderViewHolder( composeView: ComposeView, diff --git a/app/src/main/java/org/mozilla/fenix/home/recentvisits/RecentVisitsFeature.kt b/app/src/main/java/org/mozilla/fenix/home/recentvisits/RecentVisitsFeature.kt index 3c0a3cb2b9..f1bdcd0086 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recentvisits/RecentVisitsFeature.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recentvisits/RecentVisitsFeature.kt @@ -38,12 +38,12 @@ import kotlin.math.max * which will be mapped to [RecentlyVisitedItem]s and then dispatched to [AppStore] * to be displayed on the home screen. * - * @property appStore The [AppStore] that holds the state of the [HomeFragment]. - * @property historyMetadataStorage The storage that manages [HistoryMetadata]. - * @property historyHighlightsStorage The storage that manages [PlacesHistoryStorage]. - * @property scope The [CoroutineScope] used for IO operations related to querying history + * @param appStore The [AppStore] that holds the state of the [HomeFragment]. + * @param historyMetadataStorage The storage that manages [HistoryMetadata]. + * @param historyHighlightsStorage The storage that manages [PlacesHistoryStorage]. + * @param scope The [CoroutineScope] used for IO operations related to querying history * and then for dispatching updates. - * @property ioDispatcher The [CoroutineDispatcher] for performing read/write operations. + * @param ioDispatcher The [CoroutineDispatcher] for performing read/write operations. */ class RecentVisitsFeature( private val appStore: AppStore, diff --git a/app/src/main/java/org/mozilla/fenix/home/recentvisits/view/RecentVisitsHeaderViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/recentvisits/view/RecentVisitsHeaderViewHolder.kt index 9ec290eff6..f61acc3179 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recentvisits/view/RecentVisitsHeaderViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recentvisits/view/RecentVisitsHeaderViewHolder.kt @@ -24,7 +24,7 @@ import org.mozilla.fenix.home.recentvisits.interactor.RecentVisitsInteractor * * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. * @param viewLifecycleOwner [LifecycleOwner] to which this Composable will be tied to. - * @property interactor [RecentVisitsInteractor] which will have delegated to all user + * @param interactor [RecentVisitsInteractor] which will have delegated to all user * interactions. */ class RecentVisitsHeaderViewHolder( diff --git a/app/src/main/java/org/mozilla/fenix/home/recentvisits/view/RecentlyVisitedViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/recentvisits/view/RecentlyVisitedViewHolder.kt index 29b82f8697..14e2b02e07 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recentvisits/view/RecentlyVisitedViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recentvisits/view/RecentlyVisitedViewHolder.kt @@ -27,7 +27,7 @@ import org.mozilla.fenix.wallpapers.WallpaperState * * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. * @param viewLifecycleOwner [LifecycleOwner] to which this Composable will be tied to. - * @property interactor [RecentVisitsInteractor] which will have delegated to all user interactions. + * @param interactor [RecentVisitsInteractor] which will have delegated to all user interactions. */ class RecentlyVisitedViewHolder( composeView: ComposeView, diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt index 6eadcc4d2d..da17a8b196 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt @@ -13,13 +13,13 @@ import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.top.sites.TopSite import mozilla.components.service.nimbus.messaging.Message import mozilla.components.service.pocket.PocketStory +import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.appstate.AppState import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.shouldShowRecentSyncedTabs import org.mozilla.fenix.ext.shouldShowRecentTabs -import org.mozilla.fenix.home.Mode import org.mozilla.fenix.home.recentbookmarks.RecentBookmark import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem import org.mozilla.fenix.messaging.FenixMessageSurfaceId @@ -131,7 +131,7 @@ private fun showCollections( private fun privateModeAdapterItems() = listOf(AdapterItem.PrivateBrowsingDescription) private fun AppState.toAdapterList(settings: Settings): List = when (mode) { - is Mode.Normal -> normalModeAdapterItems( + BrowsingMode.Normal -> normalModeAdapterItems( settings, topSites, collections, @@ -145,7 +145,7 @@ private fun AppState.toAdapterList(settings: Settings): List = when pocketStories, firstFrameDrawn, ) - is Mode.Private -> privateModeAdapterItems() + BrowsingMode.Private -> privateModeAdapterItems() } private fun collectionTabItems(collection: TabCollection) = @@ -158,7 +158,7 @@ private fun collectionTabItems(collection: TabCollection) = * * @param containerView The [View] that is used to initialize the Home recycler view. * @param viewLifecycleOwner [LifecycleOwner] for the view. - * @property interactor [SessionControlInteractor] which will have delegated to all user + * @param interactor [SessionControlInteractor] which will have delegated to all user * interactions. */ class SessionControlView( diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/MessageCardViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/MessageCardViewHolder.kt index 236e65ec2a..7fbe7e145e 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/MessageCardViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/MessageCardViewHolder.kt @@ -29,7 +29,7 @@ import org.mozilla.fenix.wallpapers.WallpaperState * * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. * @param viewLifecycleOwner [LifecycleOwner] to which this Composable will be tied to. - * @property interactor [SessionControlInteractor] which will have delegated to all user + * @param interactor [SessionControlInteractor] which will have delegated to all user * interactions. */ class MessageCardViewHolder( diff --git a/app/src/main/java/org/mozilla/fenix/home/topsites/TopSiteItemMenu.kt b/app/src/main/java/org/mozilla/fenix/home/topsites/TopSiteItemMenu.kt index 6fbdcaa9ea..3826da5c61 100644 --- a/app/src/main/java/org/mozilla/fenix/home/topsites/TopSiteItemMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/home/topsites/TopSiteItemMenu.kt @@ -13,9 +13,9 @@ import org.mozilla.fenix.R /** * Helper class for building a context menu for a top site item. * - * @property context An Android context. - * @property topSite The [TopSite] to show the context menu for. - * @property onItemTapped Callback invoked when the user taps on a menu item. + * @param context An Android context. + * @param topSite The [TopSite] to show the context menu for. + * @param onItemTapped Callback invoked when the user taps on a menu item. */ class TopSiteItemMenu( private val context: Context, diff --git a/app/src/main/java/org/mozilla/fenix/home/topsites/TopSites.kt b/app/src/main/java/org/mozilla/fenix/home/topsites/TopSites.kt index 85d42534b2..53d4aaddcb 100644 --- a/app/src/main/java/org/mozilla/fenix/home/topsites/TopSites.kt +++ b/app/src/main/java/org/mozilla/fenix/home/topsites/TopSites.kt @@ -38,12 +38,8 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -61,8 +57,6 @@ import org.mozilla.fenix.compose.Favicon import org.mozilla.fenix.compose.MenuItem import org.mozilla.fenix.compose.PagerIndicator import org.mozilla.fenix.compose.annotation.LightDarkPreview -import org.mozilla.fenix.ext.bitmapForUrl -import org.mozilla.fenix.ext.components import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.wallpapers.WallpaperState @@ -374,9 +368,9 @@ private fun TopSiteFaviconCard( shape = RoundedCornerShape(4.dp), ) { if (topSite is TopSite.Provided) { - FaviconBitmap(topSite) + TopSiteFavicon(topSite.url, topSite.imageUrl) } else { - FavIconForUrl(topSite.url) + TopSiteFavicon(topSite.url) } } } @@ -394,45 +388,7 @@ private fun FaviconImage(painter: Painter) { } @Composable -private fun FaviconBitmap(topSite: TopSite.Provided) { - var faviconBitmapUiState by remember { mutableStateOf(FaviconBitmapUiState.Loading) } - - val client = LocalContext.current.components.core.client - - LaunchedEffect(topSite.imageUrl) { - val bitmapForUrl = client.bitmapForUrl(topSite.imageUrl) - - faviconBitmapUiState = if (bitmapForUrl == null) { - FaviconBitmapUiState.Error - } else { - FaviconBitmapUiState.Data(bitmapForUrl.asImageBitmap()) - } - } - - when (val uiState = faviconBitmapUiState) { - is FaviconBitmapUiState.Data -> FaviconImage(BitmapPainter(uiState.imageBitmap)) - is FaviconBitmapUiState.Error -> FaviconDefault(topSite.url) - is FaviconBitmapUiState.Loading -> { - // no-op - // Don't update the icon while loading else the top site icon could have a 'flashing' effect - // caused by the 'place holder letter' icon being immediately updated with the desired bitmap. - } - } -} - -private sealed class FaviconBitmapUiState { - data class Data(val imageBitmap: ImageBitmap) : FaviconBitmapUiState() - object Loading : FaviconBitmapUiState() - object Error : FaviconBitmapUiState() -} - -@Composable -private fun FaviconDefault(url: String) { - Favicon(url = url, size = TOP_SITES_FAVICON_SIZE.dp) -} - -@Composable -private fun FavIconForUrl(url: String) { +private fun TopSiteFavicon(url: String, imageUrl: String? = null) { when (url) { SupportUtils.POCKET_TRENDING_URL -> FaviconImage(painterResource(R.drawable.ic_pocket)) SupportUtils.BAIDU_URL -> FaviconImage(painterResource(R.drawable.ic_baidu)) @@ -440,7 +396,7 @@ private fun FavIconForUrl(url: String) { SupportUtils.PDD_URL -> FaviconImage(painterResource(R.drawable.ic_pdd)) SupportUtils.TC_URL -> FaviconImage(painterResource(R.drawable.ic_tc)) SupportUtils.MEITUAN_URL -> FaviconImage(painterResource(R.drawable.ic_meituan)) - else -> FaviconDefault(url) + else -> Favicon(url = url, size = TOP_SITES_FAVICON_SIZE.dp, imageUrl = imageUrl) } } diff --git a/app/src/main/java/org/mozilla/fenix/home/topsites/TopSitesViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/topsites/TopSitesViewHolder.kt index dda86f3d93..0e6eb91a14 100644 --- a/app/src/main/java/org/mozilla/fenix/home/topsites/TopSitesViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/topsites/TopSitesViewHolder.kt @@ -21,7 +21,7 @@ import org.mozilla.fenix.wallpapers.WallpaperState * * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. * @param viewLifecycleOwner [LifecycleOwner] to which this Composable will be tied to. - * @property interactor [TopSiteInteractor] which will have delegated to all user top sites + * @param interactor [TopSiteInteractor] which will have delegated to all user top sites * interactions. */ class TopSitesViewHolder( diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragmentInteractor.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragmentInteractor.kt index a3e7e43892..4cf844495d 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragmentInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragmentInteractor.kt @@ -15,7 +15,7 @@ import org.mozilla.fenix.components.metrics.MetricsUtils * Interactor for the Bookmarks screen. * Provides implementations for the BookmarkViewInteractor. * - * @property bookmarksController view controller + * @param bookmarksController view controller */ @SuppressWarnings("TooManyFunctions") class BookmarkFragmentInteractor( diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt index 10031171c9..e7c327ac5f 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt @@ -55,7 +55,6 @@ import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.ext.setTextColor -import org.mozilla.fenix.home.Mode import org.mozilla.fenix.library.LibraryPageFragment import org.mozilla.fenix.library.history.state.HistoryNavigationMiddleware import org.mozilla.fenix.library.history.state.HistoryStorageMiddleware @@ -117,7 +116,7 @@ class HistoryFragment : LibraryPageFragment(), UserInteractionHandler, onBackPressed = requireActivity().onBackPressedDispatcher::onBackPressed, ), HistoryTelemetryMiddleware( - isInPrivateMode = requireComponents.appStore.state.mode == Mode.Private, + isInPrivateMode = requireComponents.appStore.state.mode == BrowsingMode.Private, ), HistorySyncMiddleware( accountManager = requireContext().components.backgroundServices.accountManager, diff --git a/app/src/main/java/org/mozilla/fenix/library/history/state/HistoryNavigationMiddleware.kt b/app/src/main/java/org/mozilla/fenix/library/history/state/HistoryNavigationMiddleware.kt index 59c5a24e08..4a3b57eb24 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/state/HistoryNavigationMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/state/HistoryNavigationMiddleware.kt @@ -23,10 +23,10 @@ import org.mozilla.fenix.library.history.HistoryFragmentStore * A [Middleware] for initiating navigation events based on [HistoryFragmentAction]s that are * dispatched to the [HistoryFragmentStore]. * - * @property navController [NavController] for handling navigation events - * @property openToBrowser Callback to open history items in a browser window. - * @property onBackPressed Callback to handle back press actions. - * @property scope [CoroutineScope] used to launch coroutines. + * @param navController [NavController] for handling navigation events + * @param openToBrowser Callback to open history items in a browser window. + * @param onBackPressed Callback to handle back press actions. + * @param scope [CoroutineScope] used to launch coroutines. */ class HistoryNavigationMiddleware( private val navController: NavController, diff --git a/app/src/main/java/org/mozilla/fenix/library/history/state/HistoryStorageMiddleware.kt b/app/src/main/java/org/mozilla/fenix/library/history/state/HistoryStorageMiddleware.kt index 204e69ae4b..0826558cd3 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/state/HistoryStorageMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/state/HistoryStorageMiddleware.kt @@ -28,14 +28,14 @@ import org.mozilla.fenix.library.history.toPendingDeletionHistory * A [Middleware] for initiating storage side-effects based on [HistoryFragmentAction]s that are * dispatched to the [HistoryFragmentStore]. * - * @property appStore To dispatch Actions to update global state. - * @property browserStore To dispatch Actions to update global state. - * @property historyProvider To update storage as a result of some Actions. - * @property historyStorage To update storage as a result of some Actions. - * @property undoDeleteSnackbar Called when items are deleted to offer opportunity of undoing before + * @param appStore To dispatch Actions to update global state. + * @param browserStore To dispatch Actions to update global state. + * @param historyProvider To update storage as a result of some Actions. + * @param historyStorage To update storage as a result of some Actions. + * @param undoDeleteSnackbar Called when items are deleted to offer opportunity of undoing before * deletion is fully completed. - * @property onTimeFrameDeleted Called when a time range of items is successfully deleted. - * @property scope CoroutineScope to launch storage operations into. + * @param onTimeFrameDeleted Called when a time range of items is successfully deleted. + * @param scope CoroutineScope to launch storage operations into. */ class HistoryStorageMiddleware( private val appStore: AppStore, diff --git a/app/src/main/java/org/mozilla/fenix/library/history/state/HistorySyncMiddleware.kt b/app/src/main/java/org/mozilla/fenix/library/history/state/HistorySyncMiddleware.kt index 775bb81619..ddea165945 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/state/HistorySyncMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/state/HistorySyncMiddleware.kt @@ -18,9 +18,9 @@ import org.mozilla.fenix.library.history.HistoryFragmentState * A [Middleware] for handling Sync side-effects based on [HistoryFragmentAction]s that are dispatched * to the [HistoryFragmentStore]. * - * @property accountManager The [FxaAccountManager] for handling Sync operations. - * @property refreshView Callback to refresh the view once a Sync is completed. - * @property scope Coroutine scope to launch Sync operations into. + * @param accountManager The [FxaAccountManager] for handling Sync operations. + * @param refreshView Callback to refresh the view once a Sync is completed. + * @param scope Coroutine scope to launch Sync operations into. */ class HistorySyncMiddleware( private val accountManager: FxaAccountManager, diff --git a/app/src/main/java/org/mozilla/fenix/library/history/state/HistoryTelemetryMiddleware.kt b/app/src/main/java/org/mozilla/fenix/library/history/state/HistoryTelemetryMiddleware.kt index 3d4f4fe910..4590f4d726 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/state/HistoryTelemetryMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/state/HistoryTelemetryMiddleware.kt @@ -18,7 +18,7 @@ import org.mozilla.fenix.GleanMetrics.History as GleanHistory * A [Middleware] for recording telemetry based on [HistoryFragmentAction]s that are dispatched to * the [HistoryFragmentStore]. * - * @property isInPrivateMode Whether the app is currently in private browsing mode. + * @param isInPrivateMode Whether the app is currently in private browsing mode. */ class HistoryTelemetryMiddleware( private val isInPrivateMode: Boolean, diff --git a/app/src/main/java/org/mozilla/fenix/nimbus/controller/NimbusBranchesController.kt b/app/src/main/java/org/mozilla/fenix/nimbus/controller/NimbusBranchesController.kt index f907446d4a..1e85442602 100644 --- a/app/src/main/java/org/mozilla/fenix/nimbus/controller/NimbusBranchesController.kt +++ b/app/src/main/java/org/mozilla/fenix/nimbus/controller/NimbusBranchesController.kt @@ -24,12 +24,12 @@ import org.mozilla.fenix.nimbus.NimbusBranchesStore * [NimbusBranchesFragment] controller. This implements [NimbusBranchesAdapterDelegate] to handle * interactions with a Nimbus branch. * - * @property context An Android [Context]. - * @property navController [NavController] used for navigation. - * @property nimbusBranchesStore An instance of [NimbusBranchesStore] for dispatching + * @param context An Android [Context]. + * @param navController [NavController] used for navigation. + * @param nimbusBranchesStore An instance of [NimbusBranchesStore] for dispatching * [NimbusBranchesAction]s. - * @property experiments An instance of [NimbusApi] for interacting with the Nimbus experiments. - * @property experimentId The string experiment-id or "slug" for a Nimbus experiment. + * @param experiments An instance of [NimbusApi] for interacting with the Nimbus experiments. + * @param experimentId The string experiment-id or "slug" for a Nimbus experiment. */ class NimbusBranchesController( private val context: Context, diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/HomeCFRPresenter.kt b/app/src/main/java/org/mozilla/fenix/onboarding/HomeCFRPresenter.kt index e8d0c84ac1..e4f54f8702 100644 --- a/app/src/main/java/org/mozilla/fenix/onboarding/HomeCFRPresenter.kt +++ b/app/src/main/java/org/mozilla/fenix/onboarding/HomeCFRPresenter.kt @@ -34,8 +34,8 @@ private const val CFR_TO_ANCHOR_VERTICAL_PADDING = -16 /** * Delegate for handling the Home Onboarding CFR. * - * @property context [Context] used for various Android interactions. - * @property recyclerView [RecyclerView] will serve as anchor for the CFR. + * @param context [Context] used for various Android interactions. + * @param recyclerView [RecyclerView] will serve as anchor for the CFR. */ class HomeCFRPresenter( private val context: Context, diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/JunoOnboardingFragment.kt b/app/src/main/java/org/mozilla/fenix/onboarding/JunoOnboardingFragment.kt index 9be2a62523..1766017be9 100644 --- a/app/src/main/java/org/mozilla/fenix/onboarding/JunoOnboardingFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/onboarding/JunoOnboardingFragment.kt @@ -23,9 +23,11 @@ import androidx.core.app.NotificationManagerCompat import androidx.fragment.app.Fragment import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.navigation.fragment.findNavController +import mozilla.components.service.nimbus.evalJexlSafe import mozilla.components.support.base.ext.areNotificationsEnabledSafe import org.mozilla.fenix.R import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint +import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.hideToolbar import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.openSetDefaultBrowserOption @@ -221,9 +223,15 @@ class JunoOnboardingFragment : Fragment() { private fun pagesToDisplay( showNotificationPage: Boolean, showAddWidgetPage: Boolean, - ): List = - FxNimbus.features.junoOnboarding.value().cards.values.toPageUiData( + ): List { + val junoOnboardingFeature = FxNimbus.features.junoOnboarding.value() + val jexlConditions = junoOnboardingFeature.conditions + val jexlHelper = requireContext().components.analytics.messagingStorage.helper + + return FxNimbus.features.junoOnboarding.value().cards.values.toPageUiData( showNotificationPage, showAddWidgetPage, - ) + jexlConditions, + ) { condition -> jexlHelper.evalJexlSafe(condition) } + } } diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapper.kt b/app/src/main/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapper.kt index 8d8a5e4a60..685587b27a 100644 --- a/app/src/main/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapper.kt +++ b/app/src/main/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapper.kt @@ -15,21 +15,81 @@ import org.mozilla.fenix.settings.SupportUtils internal fun Collection.toPageUiData( showNotificationPage: Boolean, showAddWidgetPage: Boolean, -): List = - filter { - when (it.cardType) { - OnboardingCardType.NOTIFICATION_PERMISSION -> { - it.enabled && showNotificationPage - } - OnboardingCardType.ADD_SEARCH_WIDGET -> { - it.enabled && showAddWidgetPage + jexlConditions: Map, + func: (String) -> Boolean, +): List { + // we are first filtering the cards based on Nimbus configuration + return filter { it.shouldDisplayCard(func, jexlConditions) } + // we are then filtering again based on device capabilities + .filter { it.isCardEnabled(showNotificationPage, showAddWidgetPage) } + .sortedBy { it.ordering } + .map { it.toPageUiData() } +} + +private fun OnboardingCardData.isCardEnabled( + showNotificationPage: Boolean, + showAddWidgetPage: Boolean, +): Boolean = + when (cardType) { + OnboardingCardType.NOTIFICATION_PERMISSION -> { + enabled && showNotificationPage + } + + OnboardingCardType.ADD_SEARCH_WIDGET -> { + enabled && showAddWidgetPage + } + + else -> { + enabled + } + } + +/** + * Determines whether the given [OnboardingCardData] should be displayed. + * + * @param func Function that receives a condition as a [String] and returns its JEXL evaluation as a [Boolean]. + * @param jexlConditions A map containing the Nimbus conditions. + * + * @return True if the card should be displayed, otherwise false. + */ +private fun OnboardingCardData.shouldDisplayCard( + func: (String) -> Boolean, + jexlConditions: Map, +): Boolean { + val jexlCache: MutableMap = mutableMapOf() + + // Make sure the conditions exist and have a value, and that the number + // of valid conditions matches the number of conditions on the card's + // respective prerequisite or disqualifier table. If these mismatch, + // that means a card contains a condition that's not in the feature + // conditions lookup table. JEXLs can only be evaluated on + // supported conditions. Otherwise, consider the card invalid. + val allPrerequisites = prerequisites.mapNotNull { jexlConditions[it] } + val allDisqualifiers = disqualifiers.mapNotNull { jexlConditions[it] } + + val validPrerequisites = if (allPrerequisites.size == prerequisites.size) { + allPrerequisites.all { condition -> + jexlCache.getOrPut(condition) { + func(condition) } - else -> { - it.enabled + } + } else { + false + } + + val hasDisqualifiers = + if (allDisqualifiers.isNotEmpty() && allDisqualifiers.size == disqualifiers.size) { + allDisqualifiers.all { condition -> + jexlCache.getOrPut(condition) { + func(condition) + } } + } else { + false } - }.sortedBy { it.ordering } - .map { it.toPageUiData() } + + return validPrerequisites && !hasDisqualifiers +} private fun OnboardingCardData.toPageUiData() = OnboardingPageUiData( type = cardType.toPageUiDataType(), diff --git a/app/src/main/java/org/mozilla/fenix/push/WebPushEngineIntegration.kt b/app/src/main/java/org/mozilla/fenix/push/WebPushEngineIntegration.kt index 2d8cee9f54..6681840f10 100644 --- a/app/src/main/java/org/mozilla/fenix/push/WebPushEngineIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/push/WebPushEngineIntegration.kt @@ -58,8 +58,6 @@ internal class WebPushEngineDelegate( private val logger = Logger("WebPushEngineDelegate") override fun onGetSubscription(scope: String, onSubscription: (WebPushSubscription?) -> Unit) { - // We don't have the appServerKey unless an app is creating a new subscription so we - // allow the key to be null since it won't be overridden from a previous subscription. pushFeature.getSubscription(scope) { onSubscription(it?.toEnginePushSubscription()) } @@ -72,9 +70,7 @@ internal class WebPushEngineDelegate( ) { pushFeature.subscribe( scope = scope, - // See the full note at the implementation of `toEnginePushSubscription`. - // Issue: https://github.com/mozilla/application-services/issues/2698 - appServerKey = null, + appServerKey = serverKey?.toEncodedBase64String(), onSubscribeError = { logger.error("Error on push onSubscribe.") onSubscribe(null) @@ -104,13 +100,12 @@ internal fun AutoPushSubscription.toEnginePushSubscription() = WebPushSubscripti publicKey = this.publicKey.toDecodedByteArray(), endpoint = this.endpoint, authSecret = this.authKey.toDecodedByteArray(), - // We don't send the `serverKey` because the code path from that will query - // the push database for this key, which leads to an exception thrown. - // Our workaround for now is to not put the server key in to begin with (which - // will probably break a lot of sites). - // See: https://github.com/mozilla/application-services/issues/2698 + // We don't have the appServerKey unless an app is creating a new subscription so we + // allow the key to be null since it won't be overridden from a previous subscription. appServerKey = null, ) private fun String.toDecodedByteArray() = Base64.decode(this.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP) +private fun ByteArray.toEncodedBase64String() = + Base64.encodeToString(this, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP) diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt b/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt index a7d948e612..8a033baa44 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt @@ -22,7 +22,6 @@ import mozilla.components.support.ktx.kotlin.isUrl import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.GleanMetrics.Events -import org.mozilla.fenix.GleanMetrics.SearchShortcuts import org.mozilla.fenix.GleanMetrics.UnifiedSearch import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R @@ -52,7 +51,6 @@ interface SearchController { fun handleSearchShortcutEngineSelected(searchEngine: SearchEngine) fun handleClickSearchEngineSettings() fun handleExistingSessionSelected(tabId: String) - fun handleSearchShortcutsButtonClicked() fun handleCameraPermissionsNeeded() fun handleSearchEngineSuggestionClicked(searchEngine: SearchEngine) @@ -158,18 +156,7 @@ class SearchDialogController( } override fun handleTextChanged(text: String) { - // Display the search shortcuts on each entry of the search fragment (see #5308) - val textMatchesCurrentUrl = fragmentStore.state.url == text - val textMatchesCurrentSearch = fragmentStore.state.searchTerms == text - fragmentStore.dispatch(SearchFragmentAction.UpdateQuery(text)) - fragmentStore.dispatch( - SearchFragmentAction.ShowSearchShortcutEnginePicker( - !settings.showUnifiedSearchFeature && - (textMatchesCurrentUrl || textMatchesCurrentSearch || text.isEmpty()) && - settings.shouldShowSearchShortcuts, - ), - ) // For felt private browsing mode we're no longer going to prompt the user to enable search // suggestions while using private browsing mode. The preference to enable them will still @@ -275,16 +262,7 @@ class SearchDialogController( } } - if (settings.showUnifiedSearchFeature) { - UnifiedSearch.engineSelected.record(UnifiedSearch.EngineSelectedExtra(searchEngine.telemetryName())) - } else { - SearchShortcuts.selected.record(SearchShortcuts.SelectedExtra(searchEngine.telemetryName())) - } - } - - override fun handleSearchShortcutsButtonClicked() { - val isOpen = fragmentStore.state.showSearchShortcuts - fragmentStore.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(!isOpen)) + UnifiedSearch.engineSelected.record(UnifiedSearch.EngineSelectedExtra(searchEngine.telemetryName())) } override fun handleClickSearchEngineSettings() { diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt index 04a28a1b33..82181df204 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt @@ -70,7 +70,6 @@ import mozilla.components.support.ktx.android.net.isHttpOrHttps import mozilla.components.support.ktx.android.view.findViewInHierarchy import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.kotlin.toNormalizedUrl -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged import mozilla.components.ui.autocomplete.InlineAutocompleteEditText import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.BrowserDirection @@ -372,47 +371,12 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { else -> {} } - binding.searchEnginesShortcutButton.increaseTapArea(TAP_INCREASE_DPS) - binding.searchEnginesShortcutButton.isVisible = !showUnifiedSearchFeature - - binding.searchEnginesShortcutButton.setOnClickListener { - interactor.onSearchShortcutsButtonClicked() - } - qrFeature.set( createQrFeature(), owner = this, view = view, ) - binding.qrScanButton.isVisible = when { - showUnifiedSearchFeature -> false - requireContext().hasCamera() -> true - else -> false - } - - binding.qrScanButton.increaseTapArea(TAP_INCREASE_DPS) - - binding.qrScanButton.setOnClickListener { - if (!requireContext().hasCamera()) { return@setOnClickListener } - view.hideKeyboard() - toolbarView.view.clearFocus() - - if (requireContext().settings().shouldShowCameraPermissionPrompt) { - qrFeature.get()?.scan(binding.searchWrapper.id) - } else { - if (requireContext().isPermissionGranted(Manifest.permission.CAMERA)) { - qrFeature.get()?.scan(binding.searchWrapper.id) - } else { - interactor.onCameraPermissionsNeeded() - resetFocus() - view.hideKeyboard() - toolbarView.view.requestFocus() - } - } - requireContext().settings().setCameraPermissionNeededState = false - } - binding.fillLinkFromClipboard.setOnClickListener { Awesomebar.clipboardSuggestionClicked.record(NoExtras()) val clipboardUrl = requireContext().components.clipboardHandler.extractURL() ?: "" @@ -480,7 +444,6 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { observeClipboardState() observeAwesomeBarState() - observeShortcutsState() observeSuggestionProvidersState() consumeFrom(store) { @@ -528,7 +491,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { private fun hideClipboardSection() { binding.fillLinkFromClipboard.isVisible = false binding.fillLinkDivider.isVisible = false - binding.pillWrapperDivider.isVisible = false + binding.keyboardDivider.isVisible = false binding.clipboardUrl.isVisible = false binding.clipboardTitle.isVisible = false binding.linkIcon.isVisible = false @@ -540,11 +503,6 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { .collect { state -> awesomeBarView.updateSuggestionProvidersVisibility(state) } } - private fun observeShortcutsState() = consumeFlow(store) { flow -> - flow.ifAnyChanged { state -> arrayOf(state.areShortcutsAvailable, state.showSearchShortcuts) } - .collect { state -> updateSearchShortcutsIcon(state.areShortcutsAvailable, state.showSearchShortcuts) } - } - private fun observeAwesomeBarState() = consumeFlow(store) { flow -> /* * firstUpdate is used to make sure we keep the awesomebar hidden on the first run @@ -577,11 +535,8 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { } private fun updateAccessibilityTraversalOrder() { - val searchWrapperId = binding.searchWrapper.id if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { - binding.qrScanButton.accessibilityTraversalAfter = searchWrapperId - binding.searchEnginesShortcutButton.accessibilityTraversalAfter = searchWrapperId - binding.fillLinkFromClipboard.accessibilityTraversalAfter = searchWrapperId + binding.fillLinkFromClipboard.accessibilityTraversalAfter = binding.searchWrapper.id } else { viewLifecycleOwner.lifecycleScope.launch { binding.searchWrapper.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) @@ -692,7 +647,6 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { }.show() } } else { - binding.qrScanButton.isChecked = false activity?.let { AlertDialog.Builder(it).apply { val spannable = resources.getSpanned( @@ -741,7 +695,6 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { } private fun resetFocus() { - binding.qrScanButton.isChecked = false toolbarView.view.edit.focus() toolbarView.view.requestFocus() } @@ -754,13 +707,13 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { clear(binding.toolbar.id, TOP) connect(binding.toolbar.id, BOTTOM, PARENT_ID, BOTTOM) - clear(binding.pillWrapper.id, BOTTOM) - connect(binding.pillWrapper.id, BOTTOM, binding.toolbar.id, TOP) + clear(binding.keyboardDivider.id, BOTTOM) + connect(binding.keyboardDivider.id, BOTTOM, binding.toolbar.id, TOP) clear(binding.awesomeBar.id, TOP) clear(binding.awesomeBar.id, BOTTOM) connect(binding.awesomeBar.id, TOP, binding.searchSuggestionsHint.id, BOTTOM) - connect(binding.awesomeBar.id, BOTTOM, binding.pillWrapper.id, TOP) + connect(binding.awesomeBar.id, BOTTOM, binding.keyboardDivider.id, TOP) clear(binding.searchSuggestionsHint.id, TOP) clear(binding.searchSuggestionsHint.id, BOTTOM) @@ -768,7 +721,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { connect(binding.searchSuggestionsHint.id, BOTTOM, binding.searchHintBottomBarrier.id, TOP) clear(binding.fillLinkFromClipboard.id, TOP) - connect(binding.fillLinkFromClipboard.id, BOTTOM, binding.pillWrapper.id, TOP) + connect(binding.fillLinkFromClipboard.id, BOTTOM, binding.keyboardDivider.id, TOP) clear(binding.fillLinkDivider.id, TOP) connect(binding.fillLinkDivider.id, BOTTOM, binding.fillLinkFromClipboard.id, TOP) @@ -936,7 +889,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { ) { binding.fillLinkFromClipboard.isVisible = shouldShowView binding.fillLinkDivider.isVisible = shouldShowView - binding.pillWrapperDivider.isVisible = + binding.keyboardDivider.isVisible = !(shouldShowView && requireComponents.settings.shouldUseBottomToolbar) binding.clipboardTitle.isVisible = shouldShowView binding.linkIcon.isVisible = shouldShowView @@ -992,25 +945,6 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { inlineAutocompleteEditText.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO } - private fun updateSearchShortcutsIcon( - areShortcutsAvailable: Boolean, - showShortcuts: Boolean, - ) { - val showUnifiedSearchFeature = requireContext().settings().showUnifiedSearchFeature - - view?.apply { - binding.searchEnginesShortcutButton.isVisible = - !showUnifiedSearchFeature && areShortcutsAvailable - binding.pillWrapper.isVisible = !showUnifiedSearchFeature - binding.searchEnginesShortcutButton.isChecked = showShortcuts - - val color = if (showShortcuts) R.attr.textOnColorPrimary else R.attr.textPrimary - binding.searchEnginesShortcutButton.compoundDrawables[0]?.setTint( - requireContext().getColorFromAttr(color), - ) - } - } - /** * Gets the previous visible [NavBackStackEntry]. * This skips over any [NavBackStackEntry] that is associated with a [NavGraph] or refers to this @@ -1043,7 +977,6 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { } companion object { - private const val TAP_INCREASE_DPS = 8 private const val TAP_INCREASE_DPS_4 = 4 private const val QR_FRAGMENT_TAG = "MOZAC_QR_FRAGMENT" private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1 diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchDialogInteractor.kt b/app/src/main/java/org/mozilla/fenix/search/SearchDialogInteractor.kt index 25c881142c..0ed9e7d618 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchDialogInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchDialogInteractor.kt @@ -51,10 +51,6 @@ class SearchDialogInteractor( searchController.handleSearchShortcutEngineSelected(searchEngine) } - override fun onSearchShortcutsButtonClicked() { - searchController.handleSearchShortcutsButtonClicked() - } - override fun onClickSearchEngineSettings() { searchController.handleClickSearchEngineSettings() } diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/search/SearchFragmentStore.kt index f1f45ba8ca..b940879dca 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchFragmentStore.kt @@ -185,9 +185,9 @@ fun createInitialSearchFragmentState( showSessionSuggestionsForCurrentEngine = false, showAllSessionSuggestions = true, showSponsoredSuggestions = activity.browsingModeManager.mode == BrowsingMode.Normal && - settings.showSponsoredSuggestions, + settings.enableFxSuggest && settings.showSponsoredSuggestions, showNonSponsoredSuggestions = activity.browsingModeManager.mode == BrowsingMode.Normal && - settings.showNonSponsoredSuggestions, + settings.enableFxSuggest && settings.showNonSponsoredSuggestions, tabId = tabId, pastedText = pastedText, searchAccessPoint = searchAccessPoint, @@ -236,11 +236,6 @@ sealed class SearchFragmentAction : Action { */ data class SearchTabsEngineSelected(val engine: SearchEngine) : SearchFragmentAction() - /** - * Action when search engine picker is selected. - */ - data class ShowSearchShortcutEnginePicker(val show: Boolean) : SearchFragmentAction() - /** * Action when allow search suggestion in private mode hint is tapped. */ @@ -286,9 +281,9 @@ private fun searchStateReducer(state: SearchFragmentState, action: SearchFragmen showAllSyncedTabsSuggestions = action.settings.shouldShowSyncedTabsSuggestions, showSessionSuggestionsForCurrentEngine = false, // we'll show all local tabs showSponsoredSuggestions = action.browsingMode == BrowsingMode.Normal && - action.settings.showSponsoredSuggestions, + action.settings.enableFxSuggest && action.settings.showSponsoredSuggestions, showNonSponsoredSuggestions = action.browsingMode == BrowsingMode.Normal && - action.settings.showNonSponsoredSuggestions, + action.settings.enableFxSuggest && action.settings.showNonSponsoredSuggestions, showAllSessionSuggestions = true, ) is SearchFragmentAction.SearchShortcutEngineSelected -> @@ -383,8 +378,6 @@ private fun searchStateReducer(state: SearchFragmentState, action: SearchFragmen showSponsoredSuggestions = false, showNonSponsoredSuggestions = false, ) - is SearchFragmentAction.ShowSearchShortcutEnginePicker -> - state.copy(showSearchShortcuts = action.show && state.areShortcutsAvailable) is SearchFragmentAction.UpdateQuery -> state.copy(query = action.query) is SearchFragmentAction.AllowSearchSuggestionsInPrivateModePrompt -> diff --git a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarInteractor.kt b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarInteractor.kt index 1a431f0828..b8d3bce218 100644 --- a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarInteractor.kt @@ -48,11 +48,6 @@ interface AwesomeBarInteractor { */ fun onExistingSessionSelected(tabId: String) - /** - * Called whenever the Shortcuts button is clicked - */ - fun onSearchShortcutsButtonClicked() - /** * Called whenever search engine suggestion is tapped */ diff --git a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt index f637671d0c..12f0023946 100644 --- a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt +++ b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.search.awesomebar import android.content.Context import android.graphics.drawable.Drawable +import android.net.Uri import androidx.annotation.VisibleForTesting import androidx.appcompat.content.res.AppCompatResources import androidx.core.graphics.BlendModeColorFilterCompat.createBlendModeColorFilterCompat @@ -32,6 +33,9 @@ import mozilla.components.feature.syncedtabs.DeviceIndicators import mozilla.components.feature.syncedtabs.SyncedTabsStorageSuggestionProvider import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.support.ktx.android.content.getColorFromAttr +import mozilla.components.support.ktx.android.net.sameHostWithoutMobileSubdomainAs +import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl +import mozilla.components.support.ktx.kotlin.urlContainsQueryParameters import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode @@ -39,6 +43,7 @@ import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Core.Companion.METADATA_HISTORY_SUGGESTION_LIMIT import org.mozilla.fenix.components.Core.Companion.METADATA_SHORTCUT_SUGGESTION_LIMIT import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.containsQueryParameters import org.mozilla.fenix.ext.settings import org.mozilla.fenix.search.SearchEngineSource import org.mozilla.fenix.search.SearchFragmentState @@ -253,7 +258,7 @@ class AwesomeBarView( } } - @Suppress("ComplexMethod") + @Suppress("ComplexMethod", "LongMethod") @VisibleForTesting internal fun getProvidersToAdd( state: SearchProviderState, @@ -278,25 +283,31 @@ class AwesomeBarView( } if (state.showAllHistorySuggestions) { - if (activity.settings().historyMetadataUIFeature) { - providersToAdd.add(defaultCombinedHistoryProvider) - } else { - providersToAdd.add(defaultHistoryStorageProvider) - } + providersToAdd.add( + getHistoryProvider( + filter = getFilterToExcludeSponsoredResults(state), + ), + ) } if (state.showHistorySuggestionsForCurrentEngine) { - getHistoryProvidersForSearchEngine(state.searchEngineSource)?.let { - providersToAdd.add(it) + getFilterForCurrentEngineResults(state)?.let { + providersToAdd.add(getHistoryProvider(it)) } } if (state.showAllBookmarkSuggestions) { - providersToAdd.add(getBookmarksProvider(state.searchEngineSource)) + providersToAdd.add( + getBookmarksProvider( + filter = getFilterToExcludeSponsoredResults(state), + ), + ) } if (state.showBookmarksSuggestionsForCurrentEngine) { - providersToAdd.add(getBookmarksProvider(state.searchEngineSource, true)) + getFilterForCurrentEngineResults(state)?.let { + providersToAdd.add(getBookmarksProvider(it)) + } } if (state.showSearchSuggestions) { @@ -304,19 +315,28 @@ class AwesomeBarView( } if (state.showAllSyncedTabsSuggestions) { - providersToAdd.add(getSyncedTabsProvider(state.searchEngineSource)) + providersToAdd.add( + getSyncedTabsProvider( + filter = getFilterToExcludeSponsoredResults(state), + ), + ) } if (state.showSyncedTabsSuggestionsForCurrentEngine) { - providersToAdd.add(getSyncedTabsProvider(state.searchEngineSource, true)) + getFilterForCurrentEngineResults(state)?.let { + providersToAdd.add(getSyncedTabsProvider(it)) + } } if (activity.browsingModeManager.mode == BrowsingMode.Normal && state.showAllSessionSuggestions) { - providersToAdd.add(getLocalTabsProvider(state.searchEngineSource)) + // Unlike other providers, we don't exclude sponsored suggestions for open tabs. + providersToAdd.add(getLocalTabsProvider()) } if (activity.browsingModeManager.mode == BrowsingMode.Normal && state.showSessionSuggestionsForCurrentEngine) { - providersToAdd.add(getLocalTabsProvider(state.searchEngineSource, true)) + getFilterForCurrentEngineResults(state)?.let { + providersToAdd.add(getLocalTabsProvider(it)) + } } if (state.showSponsoredSuggestions || state.showNonSponsoredSuggestions) { @@ -338,44 +358,48 @@ class AwesomeBarView( } /** - * Get a new history suggestion provider that will return suggestions only from the current - * search engine's host. - * Used only for when unified search is active. + * Get a history suggestion provider configured for this [AwesomeBarView]. * - * @param searchEngineSource Search engine wrapper also informing about the selection type. + * @param filter Optional filter to limit the returned history suggestions. * * @return A [CombinedHistorySuggestionProvider] or [HistoryStorageSuggestionProvider] depending - * on if the history metadata feature is enabled or `null` if the current engine's host is unknown. + * on if the history metadata feature is enabled. */ @VisibleForTesting - internal fun getHistoryProvidersForSearchEngine( - searchEngineSource: SearchEngineSource, - ): AwesomeBar.SuggestionProvider? { - val searchEngineUriFilter = searchEngineSource.searchEngine?.resultsUrl ?: return null - + internal fun getHistoryProvider( + filter: SearchResultFilter? = null, + ): AwesomeBar.SuggestionProvider { return if (activity.settings().historyMetadataUIFeature) { - CombinedHistorySuggestionProvider( - historyStorage = components.core.historyStorage, - historyMetadataStorage = components.core.historyStorage, - loadUrlUseCase = loadUrlUseCase, - icons = components.core.icons, - engine = engineForSpeculativeConnects, - maxNumberOfSuggestions = METADATA_SUGGESTION_LIMIT, - showEditSuggestion = false, - suggestionsHeader = activity.getString(R.string.firefox_suggest_header), - resultsUriFilter = searchEngineUriFilter, - ) + if (filter != null) { + CombinedHistorySuggestionProvider( + historyStorage = components.core.historyStorage, + historyMetadataStorage = components.core.historyStorage, + loadUrlUseCase = loadUrlUseCase, + icons = components.core.icons, + engine = engineForSpeculativeConnects, + maxNumberOfSuggestions = METADATA_SUGGESTION_LIMIT, + showEditSuggestion = false, + suggestionsHeader = activity.getString(R.string.firefox_suggest_header), + resultsUriFilter = filter::shouldIncludeUri, + ) + } else { + defaultCombinedHistoryProvider + } } else { - HistoryStorageSuggestionProvider( - historyStorage = components.core.historyStorage, - loadUrlUseCase = loadUrlUseCase, - icons = components.core.icons, - engine = engineForSpeculativeConnects, - maxNumberOfSuggestions = METADATA_SUGGESTION_LIMIT, - showEditSuggestion = false, - suggestionsHeader = activity.getString(R.string.firefox_suggest_header), - resultsUriFilter = searchEngineUriFilter, - ) + if (filter != null) { + HistoryStorageSuggestionProvider( + historyStorage = components.core.historyStorage, + loadUrlUseCase = loadUrlUseCase, + icons = components.core.icons, + engine = engineForSpeculativeConnects, + maxNumberOfSuggestions = METADATA_SUGGESTION_LIMIT, + showEditSuggestion = false, + suggestionsHeader = activity.getString(R.string.firefox_suggest_header), + resultsUriFilter = filter::shouldIncludeUri, + ) + } else { + defaultHistoryStorageProvider + } } } @@ -455,24 +479,16 @@ class AwesomeBarView( } /** - * Get a synced tabs provider automatically configured to filter or not results from just the current search engine. + * Get a synced tabs provider configured for this [AwesomeBarView]. * - * @param searchEngineSource Search engine wrapper also informing about the selection type. - * @param filterByCurrentEngine Whether to apply a filter to the constructed provider such that - * it will return bookmarks only for the current search engine. + * @param filter Optional filter to limit the returned synced tab suggestions. * * @return [SyncedTabsStorageSuggestionProvider] providing suggestions for the [AwesomeBar]. */ @VisibleForTesting internal fun getSyncedTabsProvider( - searchEngineSource: SearchEngineSource, - filterByCurrentEngine: Boolean = false, + filter: SearchResultFilter? = null, ): SyncedTabsStorageSuggestionProvider { - val searchEngineHostFilter = when (filterByCurrentEngine) { - true -> searchEngineSource.searchEngine?.resultsUrl?.host - false -> null - } - return SyncedTabsStorageSuggestionProvider( components.backgroundServices.syncedTabsStorage, loadUrlUseCase, @@ -483,29 +499,21 @@ class AwesomeBarView( getDrawable(activity, R.drawable.ic_search_results_device_tablet), ), suggestionsHeader = activity.getString(R.string.firefox_suggest_header), - resultsHostFilter = searchEngineHostFilter, + resultsUrlFilter = filter?.let { it::shouldIncludeUrl }, ) } /** - * Get a local tabs provider automatically configured to filter or not results from just the current search engine. + * Get a local tabs provider configured for this [AwesomeBarView]. * - * @param searchEngineSource Search engine wrapper also informing about the selection type. - * @param filterByCurrentEngine Whether to apply a filter to the constructed provider such that - * it will return bookmarks only for the current search engine. + * @param filter Optional filter to limit the returned local tab suggestions. * * @return [SessionSuggestionProvider] providing suggestions for the [AwesomeBar]. */ @VisibleForTesting internal fun getLocalTabsProvider( - searchEngineSource: SearchEngineSource, - filterByCurrentEngine: Boolean = false, + filter: SearchResultFilter? = null, ): SessionSuggestionProvider { - val searchEngineUriFilter = when (filterByCurrentEngine) { - true -> searchEngineSource.searchEngine?.resultsUrl - false -> null - } - return SessionSuggestionProvider( activity.resources, components.core.store, @@ -514,29 +522,21 @@ class AwesomeBarView( getDrawable(activity, R.drawable.ic_search_results_tab), excludeSelectedSession = !fromHomeFragment, suggestionsHeader = activity.getString(R.string.firefox_suggest_header), - resultsUriFilter = searchEngineUriFilter, + resultsUriFilter = filter?.let { it::shouldIncludeUri }, ) } /** - * Get a bookmarks provider automatically configured to filter or not results from just the current search engine. + * Get a bookmarks provider configured for this [AwesomeBarView]. * - * @param searchEngineSource Search engine wrapper also informing about the selection type. - * @param filterByCurrentEngine Whether to apply a filter to the constructed provider such that - * it will return bookmarks only for the current search engine. + * @param filter Optional filter to limit the returned bookmark suggestions. * * @return [BookmarksStorageSuggestionProvider] providing suggestions for the [AwesomeBar]. */ @VisibleForTesting internal fun getBookmarksProvider( - searchEngineSource: SearchEngineSource, - filterByCurrentEngine: Boolean = false, + filter: SearchResultFilter? = null, ): BookmarksStorageSuggestionProvider { - val searchEngineHostFilter = when (filterByCurrentEngine) { - true -> searchEngineSource.searchEngine?.resultsUrl - false -> null - } - return BookmarksStorageSuggestionProvider( bookmarksStorage = components.core.bookmarksStorage, loadUrlUseCase = loadUrlUseCase, @@ -545,10 +545,28 @@ class AwesomeBarView( engine = engineForSpeculativeConnects, showEditSuggestion = false, suggestionsHeader = activity.getString(R.string.firefox_suggest_header), - resultsUriFilter = searchEngineHostFilter, + resultsUriFilter = filter?.let { it::shouldIncludeUri }, ) } + /** + * Returns a [SearchResultFilter] that only includes results for the current search engine. + */ + internal fun getFilterForCurrentEngineResults(state: SearchProviderState): SearchResultFilter? = + state.searchEngineSource.searchEngine?.resultsUrl?.let { + SearchResultFilter.CurrentEngine(it) + } + + /** + * Returns a [SearchResultFilter] that excludes sponsored results. + */ + internal fun getFilterToExcludeSponsoredResults(state: SearchProviderState): SearchResultFilter? = + if (state.showSponsoredSuggestions) { + SearchResultFilter.ExcludeSponsored(activity.settings().frecencyFilterQuery) + } else { + null + } + data class SearchProviderState( val showSearchShortcuts: Boolean, val showSearchTermHistory: Boolean, @@ -566,6 +584,41 @@ class AwesomeBarView( val searchEngineSource: SearchEngineSource, ) + /** + * Filters to limit the suggestions returned from a suggestion provider. + */ + sealed interface SearchResultFilter { + /** + * A filter for the currently selected search engine. This filter only includes suggestions + * whose URLs have the same host as [resultsUri]. + */ + data class CurrentEngine(val resultsUri: Uri) : SearchResultFilter + + /** + * A filter that excludes sponsored suggestions, whose URLs contain the given + * [queryParameter]. + */ + data class ExcludeSponsored(val queryParameter: String) : SearchResultFilter + + /** + * Returns `true` if the suggestion with the given [uri] should be included in the + * suggestions returned from the provider. + */ + fun shouldIncludeUri(uri: Uri): Boolean = when (this) { + is CurrentEngine -> this.resultsUri.sameHostWithoutMobileSubdomainAs(uri) + is ExcludeSponsored -> !uri.containsQueryParameters(queryParameter) + } + + /** + * Returns `true` if the suggestion with the given [url] string should be included in the + * suggestions returned from the provider. + */ + fun shouldIncludeUrl(url: String): Boolean = when (this) { + is CurrentEngine -> resultsUri.host == url.tryGetHostFromUrl() + is ExcludeSponsored -> !url.urlContainsQueryParameters(queryParameter) + } + } + companion object { // Maximum number of suggestions returned. const val METADATA_SUGGESTION_LIMIT = 3 diff --git a/app/src/main/java/org/mozilla/fenix/search/toolbar/SearchSelectorMenu.kt b/app/src/main/java/org/mozilla/fenix/search/toolbar/SearchSelectorMenu.kt index b7a2129604..9963f00247 100644 --- a/app/src/main/java/org/mozilla/fenix/search/toolbar/SearchSelectorMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/search/toolbar/SearchSelectorMenu.kt @@ -23,8 +23,8 @@ typealias MozSearchEngine = SearchEngine /** * A popup menu composed of [SearchSelectorMenu.Item] objects. * - * @property context [Context] used for various Android interactions. - * @property interactor [ToolbarInteractor] for handling menu item interactions. + * @param context [Context] used for various Android interactions. + * @param interactor [ToolbarInteractor] for handling menu item interactions. */ class SearchSelectorMenu( private val context: Context, diff --git a/app/src/main/java/org/mozilla/fenix/search/toolbar/SearchSelectorToolbarAction.kt b/app/src/main/java/org/mozilla/fenix/search/toolbar/SearchSelectorToolbarAction.kt index 9aad9aaa2f..d67af72a0d 100644 --- a/app/src/main/java/org/mozilla/fenix/search/toolbar/SearchSelectorToolbarAction.kt +++ b/app/src/main/java/org/mozilla/fenix/search/toolbar/SearchSelectorToolbarAction.kt @@ -31,9 +31,9 @@ import org.mozilla.fenix.search.SearchDialogFragmentStore /** * A [Toolbar.Action] implementation that shows a [SearchSelector]. * - * @property store [SearchDialogFragmentStore] containing the complete state of the search dialog. - * @property defaultSearchEngine The user selected or default [SearchEngine]. - * @property menu An instance of [SearchSelectorMenu] to display a popup menu for the search + * @param store [SearchDialogFragmentStore] containing the complete state of the search dialog. + * @param defaultSearchEngine The user selected or default [SearchEngine]. + * @param menu An instance of [SearchSelectorMenu] to display a popup menu for the search * selections. */ class SearchSelectorToolbarAction( diff --git a/app/src/main/java/org/mozilla/fenix/settings/CookieBannersFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/CookieBannersFragment.kt deleted file mode 100644 index 7b8853029b..0000000000 --- a/app/src/main/java/org/mozilla/fenix/settings/CookieBannersFragment.kt +++ /dev/null @@ -1,64 +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.settings - -import android.os.Bundle -import androidx.preference.Preference -import androidx.preference.PreferenceFragmentCompat -import androidx.preference.SwitchPreferenceCompat -import mozilla.components.concept.engine.Settings -import org.mozilla.fenix.GleanMetrics.CookieBanners -import org.mozilla.fenix.R -import org.mozilla.fenix.ext.components -import org.mozilla.fenix.ext.settings -import org.mozilla.fenix.ext.showToolbar - -/** - * Lets the user set up the cookie banners handling preferences. - */ -class CookieBannersFragment : PreferenceFragmentCompat() { - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - setPreferencesFromResource(R.xml.cookie_banner_preferences, rootKey) - setupPreferences() - } - - override fun onResume() { - super.onResume() - showToolbar(getString(R.string.preferences_cookie_banner_reduction)) - } - - private fun getEngineSettings(): Settings { - return requireContext().components.core.engine.settings - } - - private fun setupPreferences() { - requirePreference(R.string.pref_key_cookie_banner_v1).apply { - summary = - getString(R.string.reduce_cookie_banner_summary_1, getString(R.string.app_name)) - onPreferenceChangeListener = object : SharedPreferenceUpdater() { - override fun onPreferenceChange( - preference: Preference, - newValue: Any?, - ): Boolean { - val metricTag = if (newValue == true) { - "reject_all" - } else { - "disabled" - } - requireContext().settings().shouldUseCookieBanner = newValue as Boolean - val mode = requireContext().settings().getCookieBannerHandling() - getEngineSettings().cookieBannerHandlingModePrivateBrowsing = mode - getEngineSettings().cookieBannerHandlingMode = mode - getEngineSettings().cookieBannerHandlingDetectOnlyMode = - requireContext().settings().shouldShowCookieBannerReEngagementDialog() - CookieBanners.settingChanged.record(CookieBanners.SettingChangedExtra(metricTag)) - requireContext().components.useCases.sessionUseCases.reload() - return super.onPreferenceChange(preference, newValue) - } - } - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/settings/CookieBannersSwitchPreference.kt b/app/src/main/java/org/mozilla/fenix/settings/CookieBannersSwitchPreference.kt deleted file mode 100644 index 36e5599b21..0000000000 --- a/app/src/main/java/org/mozilla/fenix/settings/CookieBannersSwitchPreference.kt +++ /dev/null @@ -1,26 +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.settings - -import android.content.Context -import android.util.AttributeSet -import org.mozilla.fenix.ext.settings - -/** - * Cookie banners switch preference with a learn more link. - */ -class CookieBannersSwitchPreference(context: Context, attrs: AttributeSet?) : - LearnMoreSwitchPreference(context, attrs) { - - override fun getLearnMoreUrl(): String { - return SupportUtils.getGenericSumoURLForTopic( - SupportUtils.SumoTopic.COOKIE_BANNER, - ) - } - - override fun getSwitchValue(): Boolean { - return context.settings().shouldUseCookieBanner - } -} diff --git a/app/src/main/java/org/mozilla/fenix/settings/LearnMoreSwitchPreference.kt b/app/src/main/java/org/mozilla/fenix/settings/LearnMoreSwitchPreference.kt deleted file mode 100644 index 6bdb510bd1..0000000000 --- a/app/src/main/java/org/mozilla/fenix/settings/LearnMoreSwitchPreference.kt +++ /dev/null @@ -1,83 +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.settings - -import android.content.Context -import android.util.AttributeSet -import android.widget.TextView -import androidx.appcompat.widget.SwitchCompat -import androidx.core.view.isVisible -import androidx.preference.PreferenceViewHolder -import androidx.preference.SwitchPreferenceCompat -import org.mozilla.fenix.BrowserDirection -import org.mozilla.fenix.HomeActivity -import org.mozilla.fenix.R -import org.mozilla.fenix.ext.asActivity - -/** - * A [SwitchPreferenceCompat] that include a learn more link. - */ -abstract class LearnMoreSwitchPreference(context: Context, attrs: AttributeSet?) : - SwitchPreferenceCompat(context, attrs) { - - init { - layoutResource = R.layout.preference_switch_learn_more - } - - override fun onBindViewHolder(holder: PreferenceViewHolder) { - super.onBindViewHolder(holder) - holder.itemView.isClickable = false - val switch = holder.findViewById(R.id.learn_more_switch) as SwitchCompat - - switch.run { - isChecked = getSwitchValue() - setOnCheckedChangeListener { _, isChecked -> - onPreferenceChangeListener?.onPreferenceChange( - this@LearnMoreSwitchPreference, - isChecked, - ) - } - } - - getDescription()?.let { - val summaryView = holder.findViewById(android.R.id.summary) as TextView - summaryView.text = it - summaryView.isVisible = true - } - - val learnMoreLink = holder.findViewById(R.id.link) as TextView - learnMoreLink.paint?.isUnderlineText = true - learnMoreLink.setOnClickListener { - it.context.asActivity()?.let { activity -> - (activity as HomeActivity).openToBrowserAndLoad( - searchTermOrURL = getLearnMoreUrl(), - newTab = true, - from = BrowserDirection.FromCookieBanner, - ) - } - } - - val backgroundDrawableArray = - context.obtainStyledAttributes(intArrayOf(R.attr.selectableItemBackground)) - val backgroundDrawable = backgroundDrawableArray.getDrawable(0) - backgroundDrawableArray.recycle() - learnMoreLink.background = backgroundDrawable - } - - /** - * Returns the description to be used the UI. - */ - open fun getDescription(): String? = null - - /** - * Returns the URL that should be used when the learn more link is clicked. - */ - abstract fun getLearnMoreUrl(): String - - /** - * Indicates the value which the switch widget should show. - */ - abstract fun getSwitchValue(): Boolean -} diff --git a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt index 2fc3364135..4f2461d4a1 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt @@ -284,11 +284,6 @@ class SettingsFragment : PreferenceFragmentCompat() { resources.getString(R.string.pref_key_https_only_settings) -> { SettingsFragmentDirections.actionSettingsFragmentToHttpsOnlyFragment() } - resources.getString(R.string.pref_key_cookie_banner_settings) -> { - FxNimbus.features.cookieBanners.recordExposure() - CookieBanners.visitedSetting.record(mozilla.components.service.glean.private.NoExtras()) - SettingsFragmentDirections.actionSettingsFragmentToCookieBannerFragment() - } resources.getString(R.string.pref_key_accessibility) -> { SettingsFragmentDirections.actionSettingsFragmentToAccessibilityFragment() } @@ -675,13 +670,30 @@ class SettingsFragment : PreferenceFragmentCompat() { @VisibleForTesting internal fun setupCookieBannerPreference() { - with(requirePreference(R.string.pref_key_cookie_banner_settings)) { + FxNimbus.features.cookieBanners.recordExposure() + if (context?.settings()?.shouldShowCookieBannerUI == false) return + with(requirePreference(R.string.pref_key_cookie_banner_private_mode)) { isVisible = context.settings().shouldShowCookieBannerUI - summary = if (context.settings().shouldUseCookieBanner) { - getString(R.string.reduce_cookie_banner_option_on) - } else { - getString(R.string.reduce_cookie_banner_option_off) + onPreferenceChangeListener = object : SharedPreferenceUpdater() { + override fun onPreferenceChange( + preference: Preference, + newValue: Any?, + ): Boolean { + val metricTag = if (newValue == true) { + "reject_all" + } else { + "disabled" + } + val engineSettings = requireContext().components.core.engine.settings + val settings = requireContext().settings() + settings.shouldUseCookieBannerPrivateMode = newValue as Boolean + val mode = settings.getCookieBannerHandlingPrivateMode() + engineSettings.cookieBannerHandlingModePrivateBrowsing = mode + CookieBanners.settingChangedPmb.record(CookieBanners.SettingChangedPmbExtra(metricTag)) + requireContext().components.useCases.sessionUseCases.reload() + return super.onPreferenceChange(preference, newValue) + } } } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt b/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt index 67cee1a5af..f9ef5b09a4 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt @@ -56,7 +56,6 @@ object SupportUtils { SMARTBLOCK("smartblock-enhanced-tracking-protection"), SPONSOR_PRIVACY("sponsor-privacy"), HTTPS_ONLY_MODE("https-only-mode-firefox-android"), - COOKIE_BANNER("cookie-banner-reduction-firefox-android"), UNSIGNED_ADDONS("unsigned-addons"), REVIEW_QUALITY_CHECK("review_checker_mobile"), FX_SUGGEST("search-suggestions-firefox"), diff --git a/app/src/main/java/org/mozilla/fenix/settings/SyncPreferenceView.kt b/app/src/main/java/org/mozilla/fenix/settings/SyncPreferenceView.kt index 969726d03c..f88973c02b 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SyncPreferenceView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SyncPreferenceView.kt @@ -20,14 +20,14 @@ import mozilla.components.service.fxa.manager.SyncEnginesStorage * that manages the sync account authentication. A toggle will be also added * depending on the sync account status. * - * @property syncPreference The sync [SyncPreference] to update and handle navigation. + * @param syncPreference The sync [SyncPreference] to update and handle navigation. * @param lifecycleOwner View lifecycle owner used to determine when to cancel UI jobs. * @param accountManager An instance of [FxaAccountManager]. - * @property syncEngine The sync engine that will be used for the sync status lookup. - * @property loggedOffTitle Text label for the setting when user is not logged in. - * @property loggedInTitle Text label for the setting when user is logged in. - * @property onSyncSignInClicked A callback executed when the sync sign in [syncPreference] is clicked. - * @property onReconnectClicked A callback executed when the [syncPreference] is clicked with a + * @param syncEngine The sync engine that will be used for the sync status lookup. + * @param loggedOffTitle Text label for the setting when user is not logged in. + * @param loggedInTitle Text label for the setting when user is logged in. + * @param onSyncSignInClicked A callback executed when the sync sign in [syncPreference] is clicked. + * @param onReconnectClicked A callback executed when the [syncPreference] is clicked with a * preference status of "Reconnect". */ @Suppress("LongParameterList") diff --git a/app/src/main/java/org/mozilla/fenix/settings/account/DefaultSyncInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/account/DefaultSyncInteractor.kt index ba6067ba1a..1b92682ed2 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/account/DefaultSyncInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/account/DefaultSyncInteractor.kt @@ -11,7 +11,7 @@ interface SyncInteractor { /** * Interactor for [TurnOnSyncFragment]. * - * @property syncController Handles the interactions + * @param syncController Handles the interactions */ class DefaultSyncInteractor(private val syncController: DefaultSyncController) : SyncInteractor { override fun onCameraPermissionsNeeded() { diff --git a/app/src/main/java/org/mozilla/fenix/settings/address/controller/AddressEditorController.kt b/app/src/main/java/org/mozilla/fenix/settings/address/controller/AddressEditorController.kt index d1fa4114c7..5ee2ea230a 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/address/controller/AddressEditorController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/address/controller/AddressEditorController.kt @@ -43,10 +43,10 @@ interface AddressEditorController { /** * The default implementation of [AddressEditorController]. * - * @property storage An instance of the [AutofillCreditCardsAddressesStorage] for adding and retrieving + * @param storage An instance of the [AutofillCreditCardsAddressesStorage] for adding and retrieving * addresses. - * @property lifecycleScope [CoroutineScope] scope to launch coroutines. - * @property navController [NavController] used for navigation. + * @param lifecycleScope [CoroutineScope] scope to launch coroutines. + * @param navController [NavController] used for navigation. */ class DefaultAddressEditorController( private val storage: AutofillCreditCardsAddressesStorage, diff --git a/app/src/main/java/org/mozilla/fenix/settings/address/controller/AddressManagementController.kt b/app/src/main/java/org/mozilla/fenix/settings/address/controller/AddressManagementController.kt index aa95950590..aaf77306eb 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/address/controller/AddressManagementController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/address/controller/AddressManagementController.kt @@ -30,7 +30,7 @@ interface AddressManagementController { /** * The default implementation of [AddressManagementController]. * - * @property navController [NavController] used for navigation. + * @param navController [NavController] used for navigation. */ class DefaultAddressManagementController( private val navController: NavController, diff --git a/app/src/main/java/org/mozilla/fenix/settings/address/interactor/AddressEditorInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/address/interactor/AddressEditorInteractor.kt index 287d2068ed..4009c2678c 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/address/interactor/AddressEditorInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/address/interactor/AddressEditorInteractor.kt @@ -48,7 +48,7 @@ interface AddressEditorInteractor { /** * The default implementation of [AddressEditorInteractor]. * - * @property controller An instance of [AddressEditorController] which will be delegated for all + * @param controller An instance of [AddressEditorController] which will be delegated for all * user interactions. */ class DefaultAddressEditorInteractor( diff --git a/app/src/main/java/org/mozilla/fenix/settings/address/interactor/AddressManagementInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/address/interactor/AddressManagementInteractor.kt index 24329887e3..629eaf226d 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/address/interactor/AddressManagementInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/address/interactor/AddressManagementInteractor.kt @@ -30,7 +30,7 @@ interface AddressManagementInteractor { /** * The default implementation of [AddressManagementInteractor]. * - * @property controller An instance of [AddressManagementController] which will be delegated for + * @param controller An instance of [AddressManagementController] which will be delegated for * all user interactions. */ class DefaultAddressManagementInteractor( diff --git a/app/src/main/java/org/mozilla/fenix/settings/address/view/AddressEditorView.kt b/app/src/main/java/org/mozilla/fenix/settings/address/view/AddressEditorView.kt index 43a1c2457f..18858e0ceb 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/address/view/AddressEditorView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/address/view/AddressEditorView.kt @@ -31,10 +31,10 @@ import org.mozilla.fenix.settings.address.toCountryCode /** * An address editor for adding or updating an address. * - * @property binding The binding used to display the view. - * @property interactor [AddressEditorInteractor] used to respond to any user interactions. - * @property region If the [RegionState] is available, it will be used to set the country when adding a new address. - * @property address An [Address] to edit. + * @param binding The binding used to display the view. + * @param interactor [AddressEditorInteractor] used to respond to any user interactions. + * @param region If the [RegionState] is available, it will be used to set the country when adding a new address. + * @param address An [Address] to edit. */ class AddressEditorView( private val binding: FragmentAddressEditorBinding, diff --git a/app/src/main/java/org/mozilla/fenix/settings/biometric/BiometricPromptFeature.kt b/app/src/main/java/org/mozilla/fenix/settings/biometric/BiometricPromptFeature.kt index f56296faaf..3902989273 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/biometric/BiometricPromptFeature.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/biometric/BiometricPromptFeature.kt @@ -22,10 +22,10 @@ import org.mozilla.fenix.settings.biometric.ext.isHardwareAvailable /** * A [LifecycleAwareFeature] for the Android Biometric API to prompt for user authentication. * - * @property context Android context. - * @property fragment The fragment on which this feature will live. - * @property onAuthFailure A failure callback if authentication failed. - * @property onAuthSuccess A success callback. + * @param context Android context. + * @param fragment The fragment on which this feature will live. + * @param onAuthFailure A failure callback if authentication failed. + * @param onAuthSuccess A success callback. */ class BiometricPromptFeature( private val context: Context, diff --git a/app/src/main/java/org/mozilla/fenix/settings/cookiebannerhandling/CustomCBHSwitchPreference.kt b/app/src/main/java/org/mozilla/fenix/settings/cookiebannerhandling/CustomCBHSwitchPreference.kt new file mode 100644 index 0000000000..83916af488 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/cookiebannerhandling/CustomCBHSwitchPreference.kt @@ -0,0 +1,25 @@ +/* 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.settings.cookiebannerhandling + +import android.content.Context +import android.util.AttributeSet +import androidx.preference.SwitchPreference +import org.mozilla.fenix.ext.settings + +/** + * Custom [SwitchPreference] that automatically creates the switch for the + * cookie banner handling feature depending on the current Nimbus configurations. + */ +class CustomCBHSwitchPreference @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, +) : SwitchPreference(context, attrs) { + init { + with(context) { + setDefaultValue(settings().shouldUseCookieBannerPrivateModeDefaultValue) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/controller/CreditCardEditorController.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/controller/CreditCardEditorController.kt index 17eb913485..8a1bb8e764 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/creditcards/controller/CreditCardEditorController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/controller/CreditCardEditorController.kt @@ -48,12 +48,12 @@ interface CreditCardEditorController { /** * The default implementation of [CreditCardEditorController]. * - * @property storage An instance of the [AutofillCreditCardsAddressesStorage] for adding and retrieving + * @param storage An instance of the [AutofillCreditCardsAddressesStorage] for adding and retrieving * credit cards. - * @property lifecycleScope [CoroutineScope] scope to launch coroutines. - * @property navController [NavController] used for navigation. - * @property ioDispatcher [CoroutineDispatcher] used for executing async tasks. Defaults to [Dispatchers.IO]. - * @property showDeleteDialog [DialogInterface.OnClickListener] used to display a confirmation dialog + * @param lifecycleScope [CoroutineScope] scope to launch coroutines. + * @param navController [NavController] used for navigation. + * @param ioDispatcher [CoroutineDispatcher] used for executing async tasks. Defaults to [Dispatchers.IO]. + * @param showDeleteDialog [DialogInterface.OnClickListener] used to display a confirmation dialog * before removing credit card. */ class DefaultCreditCardEditorController( diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/interactor/CreditCardEditorInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/interactor/CreditCardEditorInteractor.kt index cb9480a502..c4525df149 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/creditcards/interactor/CreditCardEditorInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/interactor/CreditCardEditorInteractor.kt @@ -48,7 +48,7 @@ interface CreditCardEditorInteractor { /** * The default implementation of [CreditCardEditorInteractor]. * - * @property controller An instance of [CreditCardEditorController] which will be delegated for all + * @param controller An instance of [CreditCardEditorController] which will be delegated for all * user interactions. */ class DefaultCreditCardEditorInteractor( diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/interactor/CreditCardsManagementInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/interactor/CreditCardsManagementInteractor.kt index d2da3d4ed7..535a9a0ddc 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/creditcards/interactor/CreditCardsManagementInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/interactor/CreditCardsManagementInteractor.kt @@ -32,7 +32,7 @@ interface CreditCardsManagementInteractor { /** * The default implementation of [CreditCardsManagementInteractor]. * - * @property controller An instance of [CreditCardsManagementController] which will be delegated for + * @param controller An instance of [CreditCardsManagementController] which will be delegated for * all user interactions. */ class DefaultCreditCardsManagementInteractor( diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/controller/LoginsListController.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/controller/LoginsListController.kt index 6abaf9bb67..2208e08e2b 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/controller/LoginsListController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/controller/LoginsListController.kt @@ -19,10 +19,10 @@ import org.mozilla.fenix.utils.Settings /** * Controller for the saved logins list * - * @property loginsFragmentStore Store used to hold in-memory collection state. - * @property navController NavController manages app navigation within a NavHost. - * @property browserNavigator Controller allowing browser navigation to any Uri. - * @property settings SharedPreferences wrapper for easier usage. + * @param loginsFragmentStore Store used to hold in-memory collection state. + * @param navController NavController manages app navigation within a NavHost. + * @param browserNavigator Controller allowing browser navigation to any Uri. + * @param settings SharedPreferences wrapper for easier usage. */ class LoginsListController( private val loginsFragmentStore: LoginsFragmentStore, diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/interactor/AddLoginInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/interactor/AddLoginInteractor.kt index 75729028b4..48240f36fc 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/interactor/AddLoginInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/interactor/AddLoginInteractor.kt @@ -9,7 +9,7 @@ import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController /** * Interactor for the add login screen * - * @property savedLoginsController controller for the saved logins storage + * @param savedLoginsController controller for the saved logins storage */ class AddLoginInteractor( private val savedLoginsController: SavedLoginsStorageController, diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/interactor/EditLoginInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/interactor/EditLoginInteractor.kt index cb0b7a0a81..1d031c853f 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/interactor/EditLoginInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/interactor/EditLoginInteractor.kt @@ -9,7 +9,7 @@ import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController /** * Interactor for the edit login screen * - * @property savedLoginsController controller for the saved logins storage + * @param savedLoginsController controller for the saved logins storage */ class EditLoginInteractor( private val savedLoginsController: SavedLoginsStorageController, diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/interactor/LoginDetailInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/interactor/LoginDetailInteractor.kt index bd0149e448..5b06ebc0cd 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/interactor/LoginDetailInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/interactor/LoginDetailInteractor.kt @@ -9,7 +9,7 @@ import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController /** * Interactor for the login detail screen * - * @property savedLoginsController controller for the saved logins storage + * @param savedLoginsController controller for the saved logins storage */ class LoginDetailInteractor( private val savedLoginsController: SavedLoginsStorageController, diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/interactor/SavedLoginsInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/interactor/SavedLoginsInteractor.kt index 0e8682e474..5f112d3bb6 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/interactor/SavedLoginsInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/interactor/SavedLoginsInteractor.kt @@ -12,9 +12,9 @@ import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController /** * Interactor for the saved logins screen * - * @property loginsListController [LoginsListController] which will be delegated for all + * @param loginsListController [LoginsListController] which will be delegated for all * user interactions. - * @property savedLoginsStorageController [SavedLoginsStorageController] which will be delegated + * @param savedLoginsStorageController [SavedLoginsStorageController] which will be delegated * for all calls to the password storage component */ class SavedLoginsInteractor( diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ClearSiteDataView.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ClearSiteDataView.kt index f0d89491a8..161d9f1cc0 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ClearSiteDataView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ClearSiteDataView.kt @@ -37,7 +37,7 @@ interface ClearSiteDataViewInteractor { * MVI View to access the dialog to clear site cookies and data. * * @property context An Android [Context]. - * @property ioScope [CoroutineScope] with an IO dispatcher used for structured concurrency. + * @param ioScope [CoroutineScope] with an IO dispatcher used for structured concurrency. * @property containerView [ViewGroup] in which this View will inflate itself. * @property containerDivider Divider [View] to manipulate. * @property interactor [ClearSiteDataViewInteractor] which will have delegated to all user diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsInteractor.kt index 9a45577e96..b870de03fe 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsInteractor.kt @@ -10,7 +10,7 @@ package org.mozilla.fenix.settings.quicksettings * Implements callbacks for each of [ConnectionPanelDialogFragment]'s Views declared possible user interactions, * delegates all such user events to the [ConnectionDetailsController]. * - * @property controller [ConnectionDetailsController] which will be delegated for all users interactions, + * @param controller [ConnectionDetailsController] which will be delegated for all users interactions, * it expected to contain all business logic for how to act in response. */ class ConnectionDetailsInteractor( diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsView.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsView.kt index 037839cfa3..c74e9c8c30 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ConnectionDetailsView.kt @@ -20,8 +20,8 @@ import org.mozilla.fenix.ext.loadIntoView * Currently it does not support any user interaction. * * @param container [ViewGroup] in which this View will inflate itself. - * @property icons Icons component for loading, caching and processing website icons. - * @property interactor [WebSiteInfoInteractor] which will have delegated to all user interactions. + * @param icons Icons component for loading, caching and processing website icons. + * @param interactor [WebSiteInfoInteractor] which will have delegated to all user interactions. */ class ConnectionDetailsView( container: ViewGroup, diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt index 3768be853f..68e49dab66 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt @@ -91,21 +91,21 @@ interface QuickSettingsController { /** * Default behavior of [QuickSettingsController]. Other implementations are possible. * - * @property context [Context] used for various Android interactions. - * @property quickSettingsStore [QuickSettingsFragmentStore] holding the State for all Views displayed + * @param context [Context] used for various Android interactions. + * @param quickSettingsStore [QuickSettingsFragmentStore] holding the State for all Views displayed * in this Controller's Fragment. - * @property browserStore The application's [BrowserStore]. - * @property ioScope [CoroutineScope] with an IO dispatcher used for structured concurrency. - * @property navController NavController] used for navigation. + * @param browserStore The application's [BrowserStore]. + * @param ioScope [CoroutineScope] with an IO dispatcher used for structured concurrency. + * @param navController NavController] used for navigation. * @property sessionId ID of the session to manipulate. * @property sitePermissions [SitePermissions]? list of website permissions and their status. - * @property settings [Settings] application settings. - * @property permissionStorage [PermissionStorage] app state for website permissions exception. - * @property reload [ReloadUrlUseCase] callback allowing for reloading the current web page. - * @property requestRuntimePermissions [OnNeedToRequestPermissions] callback allowing for requesting + * @param settings [Settings] application settings. + * @param permissionStorage [PermissionStorage] app state for website permissions exception. + * @param reload [ReloadUrlUseCase] callback allowing for reloading the current web page. + * @param requestRuntimePermissions [OnNeedToRequestPermissions] callback allowing for requesting * specific Android runtime permissions. - * @property displayPermissions callback for when [WebsitePermissionsView] needs to be displayed. - * @property engine An [Engine] instance used for clearing the browsing data. + * @param displayPermissions callback for when [WebsitePermissionsView] needs to be displayed. + * @param engine An [Engine] instance used for clearing the browsing data. */ @Suppress("TooManyFunctions", "LongParameterList") class DefaultQuickSettingsController( diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt index 3d5215f2a8..7dcac92b0a 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt @@ -12,7 +12,7 @@ import org.mozilla.fenix.settings.quicksettings.protections.ProtectionsInteracto * Implements callbacks for each of [QuickSettingsSheetDialogFragment]'s Views declared possible user interactions, * delegates all such user events to the [QuickSettingsController]. * - * @property controller [QuickSettingsController] which will be delegated for all users interactions, + * @param controller [QuickSettingsController] which will be delegated for all users interactions, * it expected to contain all business logic for how to act in response. */ class QuickSettingsInteractor( diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoView.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoView.kt index 71e5a94c86..7a7a803461 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsiteInfoView.kt @@ -20,8 +20,8 @@ import org.mozilla.fenix.ext.loadIntoView * Currently it does not support any user interaction. * * @param container [ViewGroup] in which this View will inflate itself. - * @property icons Icons component for loading, caching and processing website icons. - * @property interactor [WebSiteInfoInteractor] which will have delegated to all user interactions. + * @param icons Icons component for loading, caching and processing website icons. + * @param interactor [WebSiteInfoInteractor] which will have delegated to all user interactions. */ class WebsiteInfoView( container: ViewGroup, diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsView.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsView.kt index 16716af79b..b3ae01ce3b 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsView.kt @@ -62,7 +62,7 @@ interface WebsitePermissionInteractor { * - camera * * @param containerView [ViewGroup] in which this View will inflate itself. - * @property interactor [WebsitePermissionInteractor] which will have delegated to all user interactions. + * @param interactor [WebsitePermissionInteractor] which will have delegated to all user interactions. */ class WebsitePermissionsView( containerView: ViewGroup, diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/ProtectionsView.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/ProtectionsView.kt index ea3f0b6fdb..533a7d906d 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/ProtectionsView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/ProtectionsView.kt @@ -33,6 +33,8 @@ import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.databinding.QuicksettingsProtectionsPanelBinding import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.trackingprotection.CookieBannerUIMode +import org.mozilla.fenix.trackingprotection.CookieBannerUIMode.REQUEST_UNSUPPORTED_SITE_SUBMITTED +import org.mozilla.fenix.trackingprotection.CookieBannerUIMode.SITE_NOT_SUPPORTED import org.mozilla.fenix.trackingprotection.ProtectionsState import org.mozilla.fenix.utils.Settings @@ -41,7 +43,7 @@ import org.mozilla.fenix.utils.Settings * to additional tracking protection details. * * @property containerView [ViewGroup] in which this View will inflate itself. - * @property trackingProtectionDivider trackingProtectionDivider The divider line between tracking protection layout + * @param trackingProtectionDivider trackingProtectionDivider The divider line between tracking protection layout * and other views from [QuickSettingsSheetDialogFragment]. * @property interactor [ProtectionsInteractor] which will have delegated to all user interactions. * @property settings [Settings] application settings. @@ -60,7 +62,8 @@ class ProtectionsView( bindTrackingProtectionInfo(state.isTrackingProtectionEnabled) bindCookieBannerProtection(state.cookieBannerUIMode) binding.trackingProtectionSwitch.isVisible = settings.shouldUseTrackingProtection - binding.cookieBannerItem.isVisible = shouldShowCookieBanner && + val isPrivateSession = state.tab?.content?.private == true + binding.cookieBannerItem.isVisible = isPrivateSession && shouldShowCookieBanner && state.cookieBannerUIMode != CookieBannerUIMode.HIDE binding.trackingProtectionDetails.setOnClickListener { @@ -98,16 +101,20 @@ class ProtectionsView( ) private val shouldShowCookieBanner: Boolean - get() = settings.shouldShowCookieBannerUI && settings.shouldUseCookieBanner + get() = settings.shouldShowCookieBannerUI && settings.shouldUseCookieBannerPrivateMode private fun bindCookieBannerProtection(cookieBannerMode: CookieBannerUIMode) { val context = binding.cookieBannerItem.context - val label = context.getString(R.string.preferences_cookie_banner_reduction) + val label = context.getString(R.string.cookie_banner_blocker) binding.cookieBannerItem.apply { setContent { FirefoxTheme { - if (cookieBannerMode == CookieBannerUIMode.REQUEST_UNSUPPORTED_SITE_SUBMITTED) { + if (cookieBannerMode in listOf( + REQUEST_UNSUPPORTED_SITE_SUBMITTED, + SITE_NOT_SUPPORTED, + ) + ) { CookieBannerItem( label = label, cookieBannerUIMode = cookieBannerMode, diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerDetailsInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerDetailsInteractor.kt index 90d0630b10..dce8ddd578 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerDetailsInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerDetailsInteractor.kt @@ -30,7 +30,7 @@ interface CookieBannerDetailsInteractor { * Implements callbacks for each of [CookieBannerPanelDialogFragment]'s Views declared possible user interactions, * delegates all such user events to the [CookieBannerDetailsController]. * - * @property controller [CookieBannerDetailsController] which will be delegated for all users interactions, + * @param controller [CookieBannerDetailsController] which will be delegated for all users interactions, * it expected to contain all business logic for how to act in response. */ class DefaultCookieBannerDetailsInteractor( diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerHandlingDetailsView.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerHandlingDetailsView.kt index c58a074f20..2420573e0c 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerHandlingDetailsView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannerHandlingDetailsView.kt @@ -26,11 +26,11 @@ import org.mozilla.fenix.trackingprotection.ProtectionsState * MVI View that knows how to display cookie banner handling details for a site. * * @param container [ViewGroup] in which this View will inflate itself. - * @property context An Android [Context]. - * @property ioScope [CoroutineScope] with an IO dispatcher used for structured concurrency. - * @property publicSuffixList To show short url. - * @property interactor [CookieBannerDetailsInteractor] which will have delegated to all user interactions. - * @property onDismiss Lambda invoked to dismiss the cookie banner. + * @param context An Android [Context]. + * @param ioScope [CoroutineScope] with an IO dispatcher used for structured concurrency. + * @param publicSuffixList To show short url. + * @param interactor [CookieBannerDetailsInteractor] which will have delegated to all user interactions. + * @param onDismiss Lambda invoked to dismiss the cookie banner. */ class CookieBannerHandlingDetailsView( container: ViewGroup, @@ -95,11 +95,11 @@ class CookieBannerHandlingDetailsView( val shortUrl = data.toShortUrl(publicSuffixList) val title = when (state) { CookieBannerUIMode.ENABLE -> context.getString( - R.string.reduce_cookie_banner_details_panel_title_off_for_site, + R.string.reduce_cookie_banner_details_panel_title_off_for_site_1, shortUrl, ) CookieBannerUIMode.DISABLE -> context.getString( - R.string.reduce_cookie_banner_details_panel_title_on_for_site, + R.string.reduce_cookie_banner_details_panel_title_on_for_site_1, shortUrl, ) CookieBannerUIMode.SITE_NOT_SUPPORTED -> context.getString( @@ -117,11 +117,11 @@ class CookieBannerHandlingDetailsView( val appName = context.getString(R.string.app_name) val description = when (state) { CookieBannerUIMode.ENABLE -> context.getString( - R.string.reduce_cookie_banner_details_panel_description_off_for_site, + R.string.reduce_cookie_banner_details_panel_description_off_for_site_1, appName, ) CookieBannerUIMode.DISABLE -> context.getString( - R.string.reduce_cookie_banner_details_panel_description_on_for_site_2, + R.string.reduce_cookie_banner_details_panel_description_on_for_site_3, appName, ) CookieBannerUIMode.SITE_NOT_SUPPORTED -> context.getString( diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannersStorageExt.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannersStorageExt.kt index d821d9859b..7bd7b7b21b 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannersStorageExt.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/CookieBannersStorageExt.kt @@ -23,7 +23,7 @@ suspend fun CookieBannersStorage.getCookieBannerUIMode( context: Context, tab: SessionState, ): CookieBannerUIMode { - return if (context.settings().shouldUseCookieBanner) { + return if (context.settings().shouldUseCookieBannerPrivateMode) { val isSiteDomainReported = withContext(Dispatchers.IO) { val host = tab.content.url.toUri().host.orEmpty() val siteDomain = context.components.publicSuffixList.getPublicSuffixPlusOne(host).await() @@ -36,7 +36,7 @@ suspend fun CookieBannersStorage.getCookieBannerUIMode( val hasException = withContext(Dispatchers.IO) { hasException(tab.content.url, tab.content.private) - } + } ?: return CookieBannerUIMode.HIDE if (hasException) { CookieBannerUIMode.DISABLE diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/dialog/CookieBannerReEngagementDialog.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/dialog/CookieBannerReEngagementDialog.kt deleted file mode 100644 index 16378f0816..0000000000 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/dialog/CookieBannerReEngagementDialog.kt +++ /dev/null @@ -1,112 +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.settings.quicksettings.protections.cookiebanners.dialog - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.DialogFragment -import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingMode.DISABLED -import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingMode.REJECT_ALL -import mozilla.components.concept.engine.Settings -import mozilla.telemetry.glean.private.NoExtras -import org.mozilla.fenix.GleanMetrics.CookieBanners -import org.mozilla.fenix.R -import org.mozilla.fenix.components.FenixSnackbar -import org.mozilla.fenix.ext.components -import org.mozilla.fenix.ext.getRootView -import org.mozilla.fenix.ext.settings -import org.mozilla.fenix.theme.FirefoxTheme - -/** - * Displays a cookie banner dialog fragment that contains the dialog compose and his logic. - */ -class CookieBannerReEngagementDialog : DialogFragment() { - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View = ComposeView(requireContext()).apply { - CookieBanners.visitedReEngagementDialog.record(NoExtras()) - - setContent { - FirefoxTheme { - val title = - context.getString( - R.string.reduce_cookie_banner_dialog_title, - context.getString(R.string.app_name), - ) - - val message = - context.getString( - R.string.reduce_cookie_banner_dialog_body, - context.getString(R.string.app_name), - ) - - val allowButtonText = - context.getString( - R.string.reduce_cookie_banner_dialog_change_setting_button, - ) - - CookieBannerReEngagementDialogCompose( - dialogTitle = title, - dialogText = message, - allowButtonText = allowButtonText, - declineButtonText = getString(R.string.reduce_cookie_banner_dialog_not_now_button), - onAllowButtonClicked = { - CookieBanners.allowReEngagementDialog.record(NoExtras()) - requireContext().settings().shouldUseCookieBanner = true - getEngineSettings().cookieBannerHandlingModePrivateBrowsing = REJECT_ALL - getEngineSettings().cookieBannerHandlingMode = REJECT_ALL - getEngineSettings().cookieBannerHandlingDetectOnlyMode = false - reload() - requireContext().getRootView()?.let { - FenixSnackbar.make( - view = it, - duration = LENGTH_SNACKBAR_DURATION, - isDisplayedWithBrowserToolbar = true, - ) - .setText(getString(R.string.reduce_cookie_banner_dialog_snackbar_text)) - .show() - } - dismiss() - }, - onNotNowButtonClicked = { - disabledCookieBannerHandlingDetectOnlyMode() - CookieBanners.notNowReEngagementDialog.record(NoExtras()) - dismiss() - }, - onCloseButtonClicked = { - disabledCookieBannerHandlingDetectOnlyMode() - requireContext().settings().userOptOutOfReEngageCookieBannerDialog = true - CookieBanners.optOutReEngagementDialog.record(NoExtras()) - dismiss() - }, - ) - } - } - } - - private fun disabledCookieBannerHandlingDetectOnlyMode() { - getEngineSettings().cookieBannerHandlingDetectOnlyMode = false - getEngineSettings().cookieBannerHandlingModePrivateBrowsing = DISABLED - getEngineSettings().cookieBannerHandlingMode = DISABLED - } - - private fun getEngineSettings(): Settings { - return requireContext().components.core.engine.settings - } - - private fun reload() { - return requireContext().components.useCases.sessionUseCases.reload() - } - - companion object { - private const val LENGTH_SNACKBAR_DURATION = 4000 // 4 seconds in ms - } -} diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/dialog/CookieBannerReEngagementDialogCompose.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/dialog/CookieBannerReEngagementDialogCompose.kt deleted file mode 100644 index 553c170e0d..0000000000 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/dialog/CookieBannerReEngagementDialogCompose.kt +++ /dev/null @@ -1,136 +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.settings.quicksettings.protections.cookiebanners.dialog - -import android.content.res.Configuration.UI_MODE_NIGHT_NO -import android.content.res.Configuration.UI_MODE_NIGHT_YES -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import org.mozilla.fenix.R -import org.mozilla.fenix.compose.button.TextButton -import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.theme.defaultTypography - -@Composable -@Preview(uiMode = UI_MODE_NIGHT_YES) -@Preview(uiMode = UI_MODE_NIGHT_NO) -private fun CookieBannerReEngagementDialogComposePreview() { - FirefoxTheme { - CookieBannerReEngagementDialogCompose( - dialogTitle = "Allow Iceraven to reject cookie banners?", - dialogText = - "Automatically reject cookie requests, when possible. Otherwise, " + - "accept all cookies to dismiss cookie banners.", - onAllowButtonClicked = {}, - onNotNowButtonClicked = {}, - onCloseButtonClicked = {}, - allowButtonText = "Dismiss banners", - declineButtonText = "NOT NOW", - ) - } -} - -/** - * Displays the cookie banner reducer dialog - */ -@Suppress("LongParameterList", "LongMethod") -@Composable -fun CookieBannerReEngagementDialogCompose( - dialogTitle: String, - dialogText: String, - allowButtonText: String, - declineButtonText: String, - onCloseButtonClicked: () -> Unit, - onAllowButtonClicked: () -> Unit, - onNotNowButtonClicked: () -> Unit, -) { - Dialog( - properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false), - onDismissRequest = onNotNowButtonClicked, - ) { - Surface( - color = Color.Transparent, - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .clip(RoundedCornerShape(8.dp)) - .background(color = FirefoxTheme.colors.layer1), - ) { - Column { - IconButton( - modifier = Modifier - .align(Alignment.End) - .size(48.dp), - onClick = onCloseButtonClicked, - ) { - Icon( - painter = painterResource(R.drawable.mozac_ic_cross_24), - contentDescription = stringResource(R.string.content_description_close_button), - tint = FirefoxTheme.colors.iconPrimary, - ) - } - Text( - modifier = Modifier.padding( - start = 24.dp, - end = 24.dp, - bottom = 8.dp, - ), - color = FirefoxTheme.colors.textPrimary, - text = dialogTitle, - style = defaultTypography.headline7, - ) - Text( - modifier = Modifier.padding(horizontal = 24.dp), - color = FirefoxTheme.colors.textPrimary, - fontSize = 16.sp, - text = dialogText, - style = defaultTypography.body1, - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(end = 24.dp, bottom = 12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy( - space = 8.dp, - alignment = Alignment.End, - ), - ) { - TextButton( - text = declineButtonText, - onClick = onNotNowButtonClicked, - ) - TextButton( - text = allowButtonText, - onClick = onAllowButtonClicked, - ) - } - } - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/dialog/CookieBannerReEngagementDialogUtils.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/dialog/CookieBannerReEngagementDialogUtils.kt deleted file mode 100644 index ffa3c98d8a..0000000000 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/dialog/CookieBannerReEngagementDialogUtils.kt +++ /dev/null @@ -1,55 +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.settings.quicksettings.protections.cookiebanners.dialog - -import androidx.navigation.NavController -import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingMode.REJECT_ALL -import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingStatus -import org.mozilla.fenix.R -import org.mozilla.fenix.browser.BrowserFragmentDirections -import org.mozilla.fenix.ext.nav -import org.mozilla.fenix.utils.Settings -import mozilla.components.concept.engine.Settings as EngineSettings - -/** - * An utility object for interacting with the re-engagement cookie banner dialog. - */ -object CookieBannerReEngagementDialogUtils { - /** - * Tries to show the re-engagement cookie banner dialog, when the right conditions are met, o - * otherwise the dialog won't show. - */ - fun tryToShowReEngagementDialog( - settings: Settings, - status: CookieBannerHandlingStatus, - navController: NavController, - ) { - if (status == CookieBannerHandlingStatus.DETECTED && - settings.shouldShowCookieBannerReEngagementDialog() - ) { - settings.cookieBannerReEngagementDialogShowsCount.increment() - settings.lastInteractionWithReEngageCookieBannerDialogInMs = System.currentTimeMillis() - settings.cookieBannerDetectedPreviously = true - val directions = - BrowserFragmentDirections.actionBrowserFragmentToCookieBannerDialogFragment() - navController.nav(R.id.browserFragment, directions) - } - } - - /** - * Tries to enable the detect only mode after the time limit for the cookie banner has been - * expired. - */ - fun tryToEnableDetectOnlyModeIfNeeded( - settings: Settings, - engineSettings: EngineSettings, - ) { - if (settings.shouldShowCookieBannerReEngagementDialog()) { - engineSettings.cookieBannerHandlingDetectOnlyMode = true - engineSettings.cookieBannerHandlingModePrivateBrowsing = REJECT_ALL - engineSettings.cookieBannerHandlingMode = REJECT_ALL - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/settings/studies/StudiesAdapter.kt b/app/src/main/java/org/mozilla/fenix/settings/studies/StudiesAdapter.kt index 200d1efeac..db715af00d 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/studies/StudiesAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/studies/StudiesAdapter.kt @@ -32,12 +32,12 @@ private const val VIEW_HOLDER_TYPE_STUDY = 1 * An adapter for displaying studies items. This will display information related to the state of * a study such as active. In addition, it will perform actions such as removing a study. * - * @property studiesDelegate Delegate that will provides method for handling + * @param studiesDelegate Delegate that will provides method for handling * the studies actions items. * @param studies The list of studies. * * @property studiesDelegate Delegate that will provides method for handling * the studies actions items. - * @property shouldSubmitOnInit The sole purpose of this property is to prevent the submitList function + * @param shouldSubmitOnInit The sole purpose of this property is to prevent the submitList function * to run on init, it should only be used from tests. */ @Suppress("LargeClass") diff --git a/app/src/main/java/org/mozilla/fenix/share/SaveToPDFMiddleware.kt b/app/src/main/java/org/mozilla/fenix/share/SaveToPDFMiddleware.kt index eb1a10d4ec..25f28887f4 100644 --- a/app/src/main/java/org/mozilla/fenix/share/SaveToPDFMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/share/SaveToPDFMiddleware.kt @@ -33,8 +33,8 @@ import java.io.IOException /** * [BrowserAction] middleware reacting in response to Save to PDF related [Action]s. * - * @property context An Application context. - * @property mainScope Coroutine scope to launch coroutines. + * @param context An Application context. + * @param mainScope Coroutine scope to launch coroutines. */ class SaveToPDFMiddleware( private val context: Context, diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareController.kt b/app/src/main/java/org/mozilla/fenix/share/ShareController.kt index 2e149a699c..9a1738484f 100644 --- a/app/src/main/java/org/mozilla/fenix/share/ShareController.kt +++ b/app/src/main/java/org/mozilla/fenix/share/ShareController.kt @@ -74,19 +74,19 @@ interface ShareController { /** * Default behavior of [ShareController]. Other implementations are possible. * - * @property context [Context] used for various Android interactions. - * @property shareSubject Desired message subject used when sharing through 3rd party apps, like email clients. - * @property shareData The list of [ShareData]s that can be shared. - * @property sendTabUseCases Instance of [SendTabUseCases] which allows sending tabs to account devices. - * @property saveToPdfUseCase Instance of [SessionUseCases.SaveToPdfUseCase] to generate a PDF of a given tab. - * @property printUseCase Instance of [SessionUseCases.PrintContentUseCase] to print content of a given tab. - * @property snackbar Instance of [FenixSnackbar] for displaying styled snackbars. - * @property navController [NavController] used for navigation. - * @property recentAppsStorage Instance of [RecentAppsStorage] for storing and retrieving the most recent apps. - * @property viewLifecycleScope [CoroutineScope] used for retrieving the most recent apps in the background. - * @property dispatcher Dispatcher used to execute suspending functions. - * @property fxaEntrypoint The entrypoint if we need to authenticate, it will be reported in telemetry. - * @property dismiss Callback signalling sharing can be closed. + * @param context [Context] used for various Android interactions. + * @param shareSubject Desired message subject used when sharing through 3rd party apps, like email clients. + * @param shareData The list of [ShareData]s that can be shared. + * @param sendTabUseCases Instance of [SendTabUseCases] which allows sending tabs to account devices. + * @param saveToPdfUseCase Instance of [SessionUseCases.SaveToPdfUseCase] to generate a PDF of a given tab. + * @param printUseCase Instance of [SessionUseCases.PrintContentUseCase] to print content of a given tab. + * @param snackbar Instance of [FenixSnackbar] for displaying styled snackbars. + * @param navController [NavController] used for navigation. + * @param recentAppsStorage Instance of [RecentAppsStorage] for storing and retrieving the most recent apps. + * @param viewLifecycleScope [CoroutineScope] used for retrieving the most recent apps in the background. + * @param dispatcher Dispatcher used to execute suspending functions. + * @param fxaEntrypoint The entrypoint if we need to authenticate, it will be reported in telemetry. + * @param dismiss Callback signalling sharing can be closed. */ @Suppress("TooManyFunctions", "LongParameterList") class DefaultShareController( diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckBottomSheetStateFeature.kt b/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckBottomSheetStateFeature.kt index bd8de32ca9..5d307e02ff 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckBottomSheetStateFeature.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckBottomSheetStateFeature.kt @@ -18,7 +18,7 @@ import org.mozilla.fenix.shopping.store.ReviewQualityCheckStore * the store state changes from [ReviewQualityCheckState.Initial] to [ReviewQualityCheckState.NotOptedIn]. * * @param store The store to observe. - * @property onRequestStateUpdate Callback to request the bottom sheet to be updated. + * @param onRequestStateUpdate Callback to request the bottom sheet to be updated. */ class ReviewQualityCheckBottomSheetStateFeature( store: ReviewQualityCheckStore, diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFeature.kt b/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFeature.kt index 68363b7626..e864c76b94 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFeature.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFeature.kt @@ -24,16 +24,16 @@ private const val DEBOUNCE_TIMEOUT_MILLIS = 200L * Feature implementation that provides review quality check information for supported product * pages. * - * @property appStore Reference to the application's [AppStore]. - * @property browserStore Reference to the application's [BrowserStore]. - * @property shoppingExperienceFeature Reference to the [ShoppingExperienceFeature]. - * @property onIconVisibilityChange Invoked when shopping icon visibility changes based on feature + * @param appStore Reference to the application's [AppStore]. + * @param browserStore Reference to the application's [BrowserStore]. + * @param shoppingExperienceFeature Reference to the [ShoppingExperienceFeature]. + * @param onIconVisibilityChange Invoked when shopping icon visibility changes based on feature * flag and when the loaded page is a supported product page. - * @property onBottomSheetStateChange Invoked when the bottom sheet is collapsed or expanded. - * @property debounceTimeoutMillis Function that returns the debounce timeout in milliseconds. This + * @param onBottomSheetStateChange Invoked when the bottom sheet is collapsed or expanded. + * @param debounceTimeoutMillis Function that returns the debounce timeout in milliseconds. This * make it possible to wait till [ContentState.isProductUrl] is stable before invoking * [onIconVisibilityChange]. - * @property onProductPageDetected Invoked when a product page is detected and loaded. Used to + * @param onProductPageDetected Invoked when a product page is detected and loaded. Used to * detect when to send telemetry for shopping.product_page_visits. */ @OptIn(FlowPreview::class) diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFragment.kt b/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFragment.kt index 3d676ac850..83fb5b1f85 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFragment.kt @@ -16,13 +16,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.rememberNestedScrollInteropConnection -import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.NavHostFragment.Companion.findNavController import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import org.mozilla.fenix.components.appstate.AppAction +import org.mozilla.fenix.components.lazyStore import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.shopping.di.ReviewQualityCheckMiddlewareProvider import org.mozilla.fenix.shopping.store.BottomSheetDismissSource @@ -40,14 +40,14 @@ class ReviewQualityCheckFragment : BottomSheetDialogFragment() { private var behavior: BottomSheetBehavior? = null private val bottomSheetStateFeature = ViewBoundFeatureWrapper() - private val store by lazy { + private val store by lazyStore { viewModelScope -> ReviewQualityCheckStore( middleware = ReviewQualityCheckMiddlewareProvider.provideMiddleware( settings = requireComponents.settings, browserStore = requireComponents.core.store, appStore = requireComponents.appStore, context = requireContext().applicationContext, - scope = lifecycleScope, + scope = viewModelScope, ), ) } diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ShoppingExperienceFeature.kt b/app/src/main/java/org/mozilla/fenix/shopping/ShoppingExperienceFeature.kt index 4eef7176e6..fd06ac5a1e 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/ShoppingExperienceFeature.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/ShoppingExperienceFeature.kt @@ -15,6 +15,11 @@ interface ShoppingExperienceFeature { * Returns true if the shopping experience feature is enabled. */ val isEnabled: Boolean + + /** + * Returns true if product recommendations exposure nimbus flag is enabled. + */ + val isProductRecommendationsExposureEnabled: Boolean } /** @@ -24,4 +29,7 @@ class DefaultShoppingExperienceFeature : ShoppingExperienceFeature { override val isEnabled get() = FxNimbus.features.shoppingExperience.value().enabled + + override val isProductRecommendationsExposureEnabled: Boolean + get() = FxNimbus.features.shoppingExperience.value().productRecommendationsExposure } diff --git a/app/src/main/java/org/mozilla/fenix/shopping/di/ReviewQualityCheckMiddlewareProvider.kt b/app/src/main/java/org/mozilla/fenix/shopping/di/ReviewQualityCheckMiddlewareProvider.kt index fc7236983b..946ff852c1 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/di/ReviewQualityCheckMiddlewareProvider.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/di/ReviewQualityCheckMiddlewareProvider.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.CoroutineScope import mozilla.components.browser.state.store.BrowserStore import mozilla.components.feature.tabs.TabsUseCases import org.mozilla.fenix.components.AppStore +import org.mozilla.fenix.shopping.DefaultShoppingExperienceFeature import org.mozilla.fenix.shopping.middleware.DefaultNetworkChecker import org.mozilla.fenix.shopping.middleware.DefaultReviewQualityCheckPreferences import org.mozilla.fenix.shopping.middleware.DefaultReviewQualityCheckService @@ -43,8 +44,8 @@ object ReviewQualityCheckMiddlewareProvider { scope: CoroutineScope, ): List = listOf( - providePreferencesMiddleware(settings, browserStore, scope), - provideNetworkMiddleware(browserStore, appStore, context, scope), + providePreferencesMiddleware(settings, browserStore, appStore, scope), + provideNetworkMiddleware(browserStore, context, scope), provideNavigationMiddleware(TabsUseCases.SelectOrAddUseCase(browserStore), context), provideTelemetryMiddleware(), ) @@ -52,22 +53,23 @@ object ReviewQualityCheckMiddlewareProvider { private fun providePreferencesMiddleware( settings: Settings, browserStore: BrowserStore, + appStore: AppStore, scope: CoroutineScope, ) = ReviewQualityCheckPreferencesMiddleware( reviewQualityCheckPreferences = DefaultReviewQualityCheckPreferences(settings), reviewQualityCheckVendorsService = DefaultReviewQualityCheckVendorsService(browserStore), + appStore = appStore, + shoppingExperienceFeature = DefaultShoppingExperienceFeature(), scope = scope, ) private fun provideNetworkMiddleware( browserStore: BrowserStore, - appStore: AppStore, context: Context, scope: CoroutineScope, ) = ReviewQualityCheckNetworkMiddleware( reviewQualityCheckService = DefaultReviewQualityCheckService(browserStore), networkChecker = DefaultNetworkChecker(context), - appStore = appStore, scope = scope, ) @@ -79,5 +81,6 @@ object ReviewQualityCheckMiddlewareProvider { GetReviewQualityCheckSumoUrl(context), ) - private fun provideTelemetryMiddleware() = ReviewQualityCheckTelemetryMiddleware() + private fun provideTelemetryMiddleware() = + ReviewQualityCheckTelemetryMiddleware() } diff --git a/app/src/main/java/org/mozilla/fenix/shopping/middleware/EnumMapper.kt b/app/src/main/java/org/mozilla/fenix/shopping/middleware/EnumMapper.kt new file mode 100644 index 0000000000..610decc091 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/shopping/middleware/EnumMapper.kt @@ -0,0 +1,12 @@ +/* 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.shopping.middleware + +/** + * Converts a string to an enum value, ignoring case. If the string does not match any of the + * enum values, the default value is returned. + */ +inline fun > String.asEnumOrDefault(defaultValue: T? = null): T? = + enumValues().firstOrNull { it.name.equals(this, ignoreCase = true) } ?: defaultValue diff --git a/app/src/main/java/org/mozilla/fenix/shopping/middleware/GetReviewQualityCheckSumoUrl.kt b/app/src/main/java/org/mozilla/fenix/shopping/middleware/GetReviewQualityCheckSumoUrl.kt index 155bd8231d..6c410dff93 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/middleware/GetReviewQualityCheckSumoUrl.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/middleware/GetReviewQualityCheckSumoUrl.kt @@ -16,7 +16,7 @@ private const val PARAM_UTM_TERM_VALUE = "core-sheet" /** * Class used to retrieve the SUMO review quality check link. * - * @property context Context used to localize the SUMO url. + * @param context Context used to localize the SUMO url. */ class GetReviewQualityCheckSumoUrl( private val context: Context, diff --git a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ProductAnalysisMapper.kt b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ProductAnalysisMapper.kt index 7e9907c973..e08d7740f7 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ProductAnalysisMapper.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ProductAnalysisMapper.kt @@ -10,14 +10,15 @@ import org.mozilla.fenix.shopping.store.ReviewQualityCheckState import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.HighlightType import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent.AnalysisStatus +import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent.HighlightsInfo /** * Maps [ProductAnalysis] to [ProductReviewState]. */ -fun ProductAnalysis?.toProductReviewState(isInitialAnalysis: Boolean = true): ProductReviewState = - this?.toProductReview(isInitialAnalysis) ?: ProductReviewState.Error.GenericError +fun ProductAnalysis?.toProductReviewState(): ProductReviewState = + this?.toProductReview() ?: ProductReviewState.Error.GenericError -private fun ProductAnalysis.toProductReview(isInitialAnalysis: Boolean): ProductReviewState = +private fun ProductAnalysis.toProductReview(): ProductReviewState = if (pageNotSupported) { ProductReviewState.Error.UnsupportedProductTypeError } else if (productId == null) { @@ -26,17 +27,15 @@ private fun ProductAnalysis.toProductReview(isInitialAnalysis: Boolean): Product } else { ProductReviewState.Error.GenericError } + } else if (notEnoughReviews && !needsAnalysis) { + ProductReviewState.Error.NotEnoughReviews } else { val mappedRating = adjustedRating?.toFloat() - val mappedGrade = grade?.toGrade() + val mappedGrade = grade?.asEnumOrDefault() val mappedHighlights = highlights?.toHighlights()?.toSortedMap() if (mappedGrade == null && mappedRating == null && mappedHighlights == null) { - if (isInitialAnalysis) { - ProductReviewState.NoAnalysisPresent() - } else { - ProductReviewState.Error.NotEnoughReviews - } + ProductReviewState.NoAnalysisPresent() } else { ProductReviewState.AnalysisPresent( productId = productId!!, @@ -44,18 +43,11 @@ private fun ProductAnalysis.toProductReview(isInitialAnalysis: Boolean): Product analysisStatus = needsAnalysis.toAnalysisStatus(), adjustedRating = mappedRating, productUrl = analysisURL!!, - highlights = mappedHighlights, + highlightsInfo = mappedHighlights?.let { HighlightsInfo(it) }, ) } } -private fun String.toGrade(): ReviewQualityCheckState.Grade? = - try { - ReviewQualityCheckState.Grade.valueOf(this) - } catch (e: IllegalArgumentException) { - null - } - private fun Boolean.toAnalysisStatus(): AnalysisStatus = when (this) { true -> AnalysisStatus.NEEDS_ANALYSIS diff --git a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ProductRecommendationMapper.kt b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ProductRecommendationMapper.kt new file mode 100644 index 0000000000..5e3441af58 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ProductRecommendationMapper.kt @@ -0,0 +1,50 @@ +/* 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.shopping.middleware + +import mozilla.components.concept.engine.shopping.ProductRecommendation +import org.mozilla.fenix.shopping.store.ReviewQualityCheckState +import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.RecommendedProductState +import java.text.NumberFormat +import java.util.Currency +import java.util.Locale + +private const val MINIMUM_FRACTION_DIGITS = 0 +private const val MAXIMUM_FRACTION_DIGITS = 2 + +/** + * Maps [ProductRecommendation] to [RecommendedProductState]. + */ +fun ProductRecommendation?.toRecommendedProductState(): RecommendedProductState = + this?.toRecommendedProduct() ?: RecommendedProductState.Initial + +private fun ProductRecommendation.toRecommendedProduct(): RecommendedProductState.Product = + RecommendedProductState.Product( + aid = aid, + name = name, + productUrl = url, + imageUrl = imageUrl, + formattedPrice = price.toDouble().toFormattedAmount(currency), + reviewGrade = grade.asEnumOrDefault()!!, + adjustedRating = adjustedRating.toFloat(), + isSponsored = sponsored, + analysisUrl = analysisUrl, + ) + +private fun Double.toFormattedAmount(currencyCode: String): String = + mapCurrencyCodeToNumberFormat(currencyCode).apply { + minimumFractionDigits = MINIMUM_FRACTION_DIGITS + maximumFractionDigits = MAXIMUM_FRACTION_DIGITS + }.format(this) + +private fun mapCurrencyCodeToNumberFormat(currencyCode: String): NumberFormat = + try { + val currency = Currency.getInstance(currencyCode) + NumberFormat.getCurrencyInstance(Locale.getDefault()).apply { + this.currency = currency + } + } catch (e: IllegalArgumentException) { + NumberFormat.getNumberInstance() + } diff --git a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckNavigationMiddleware.kt b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckNavigationMiddleware.kt index 76fe365745..91b846fceb 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckNavigationMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckNavigationMiddleware.kt @@ -11,7 +11,7 @@ import org.mozilla.fenix.shopping.store.ReviewQualityCheckMiddleware import org.mozilla.fenix.shopping.store.ReviewQualityCheckState private const val POWERED_BY_URL = - "https://www.fakespot.com/our-mission?utm_source=review-checker" + + "https://www.fakespot.com/review-checker?utm_source=review-checker" + "&utm_campaign=fakespot-by-mozilla&utm_medium=inproduct&utm_term=core-sheet" private const val PRIVACY_POLICY_URL = "https://www.fakespot.com/privacy-policy" private const val TERMS_OF_USE_URL = "https://www.fakespot.com/terms" @@ -19,8 +19,8 @@ private const val TERMS_OF_USE_URL = "https://www.fakespot.com/terms" /** * Middleware that handles navigation events for the review quality check feature. * - * @property selectOrAddUseCase UseCase instance used to open new tabs. - * @property getReviewQualityCheckSumoUrl Instance used to retrieve the learn more SUMO link. + * @param selectOrAddUseCase UseCase instance used to open new tabs. + * @param getReviewQualityCheckSumoUrl Instance used to retrieve the learn more SUMO link. */ class ReviewQualityCheckNavigationMiddleware( private val selectOrAddUseCase: TabsUseCases.SelectOrAddUseCase, @@ -64,5 +64,7 @@ class ReviewQualityCheckNavigationMiddleware( is ReviewQualityCheckAction.OpenOnboardingPrivacyPolicyLink -> PRIVACY_POLICY_URL is ReviewQualityCheckAction.OpenPoweredByLink -> POWERED_BY_URL + + is ReviewQualityCheckAction.RecommendedProductClick -> action.productUrl } } diff --git a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckNetworkMiddleware.kt b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckNetworkMiddleware.kt index 185f0a12c0..a843189367 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckNetworkMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckNetworkMiddleware.kt @@ -6,14 +6,12 @@ package org.mozilla.fenix.shopping.middleware import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import mozilla.components.concept.engine.shopping.ProductAnalysis import mozilla.components.lib.state.MiddlewareContext import mozilla.components.lib.state.Store -import org.mozilla.fenix.components.AppStore -import org.mozilla.fenix.components.appstate.AppAction.ShoppingAction import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction.FetchProductAnalysis import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction.RetryProductAnalysis +import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction.UpdateRecommendedProduct import org.mozilla.fenix.shopping.store.ReviewQualityCheckMiddleware import org.mozilla.fenix.shopping.store.ReviewQualityCheckState import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState @@ -22,15 +20,13 @@ import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductR /** * Middleware that handles network requests for the review quality check feature. * - * @property reviewQualityCheckService The service that handles the network requests. - * @property networkChecker The [NetworkChecker] instance to check the network status. - * @property appStore The [AppStore] instance to access state and dispatch [ShoppingAction]s. - * @property scope The [CoroutineScope] that will be used to launch coroutines. + * @param reviewQualityCheckService The service that handles the network requests. + * @param networkChecker The [NetworkChecker] instance to check the network status. + * @param scope The [CoroutineScope] that will be used to launch coroutines. */ class ReviewQualityCheckNetworkMiddleware( private val reviewQualityCheckService: ReviewQualityCheckService, private val networkChecker: NetworkChecker, - private val appStore: AppStore, private val scope: CoroutineScope, ) : ReviewQualityCheckMiddleware { @@ -60,17 +56,23 @@ class ReviewQualityCheckNetworkMiddleware( scope.launch { when (action) { FetchProductAnalysis, RetryProductAnalysis -> { - val productPageUrl = reviewQualityCheckService.selectedTabUrl() val productAnalysis = reviewQualityCheckService.fetchProductReview() val productReviewState = productAnalysis.toProductReviewState() - store.updateProductReviewState(productReviewState) - - productPageUrl?.let { - store.restoreAnalysingStateIfRequired( - productPageUrl = productPageUrl, - productReviewState = productReviewState, - productAnalysis = productAnalysis, - ) + + // Here the ProductReviewState should only updated after the analysis status API + // returns a result. This makes sure that the UI doesn't show the reanalyse + // button in case the product analysis is already in progress on the backend. + if (productReviewState.isAnalysisPresentOrNoAnalysisPresent() && + reviewQualityCheckService.analysisStatus().isPendingOrInProgress() + ) { + store.updateProductReviewState(productReviewState, true) + store.dispatch(ReviewQualityCheckAction.RestoreReanalysis) + } else { + store.updateProductReviewState(productReviewState) + } + + if (productReviewState is ProductReviewState.AnalysisPresent) { + store.updateRecommendedProductState() } } @@ -85,12 +87,6 @@ class ReviewQualityCheckNetworkMiddleware( return@launch } - // add product to the set of products that are being analysed - val productPageUrl = reviewQualityCheckService.selectedTabUrl() - productPageUrl?.let { - appStore.dispatch(ShoppingAction.AddToProductAnalysed(it)) - } - val status = pollForAnalysisStatus() if (status == null || @@ -113,14 +109,17 @@ class ReviewQualityCheckNetworkMiddleware( } else { // poll succeeded, update state val productAnalysis = reviewQualityCheckService.fetchProductReview() - val productReviewState = productAnalysis.toProductReviewState(false) + val productReviewState = productAnalysis.toProductReviewState() store.updateProductReviewState(productReviewState) } + } - // remove product from the set of products that are being analysed - productPageUrl?.let { - appStore.dispatch(ShoppingAction.RemoveFromProductAnalysed(it)) - } + is ReviewQualityCheckAction.RecommendedProductClick -> { + reviewQualityCheckService.recordRecommendedProductClick(action.productAid) + } + + is ReviewQualityCheckAction.RecommendedProductImpression -> { + reviewQualityCheckService.recordRecommendedProductImpression(action.productAid) } } } @@ -128,29 +127,36 @@ class ReviewQualityCheckNetworkMiddleware( private suspend fun pollForAnalysisStatus(): AnalysisStatusDto? = retry( - predicate = { it == AnalysisStatusDto.PENDING || it == AnalysisStatusDto.IN_PROGRESS }, + predicate = { it.isPendingOrInProgress() }, block = { reviewQualityCheckService.analysisStatus() }, ) private fun Store.updateProductReviewState( productReviewState: ProductReviewState, + restoreAnalysis: Boolean = false, ) { - dispatch(ReviewQualityCheckAction.UpdateProductReview(productReviewState)) + dispatch(ReviewQualityCheckAction.UpdateProductReview(productReviewState, restoreAnalysis)) } - private fun Store.restoreAnalysingStateIfRequired( - productPageUrl: String, - productReviewState: ProductReviewState, - productAnalysis: ProductAnalysis?, - ) { - if (productReviewState.isAnalysisPresentOrNoAnalysisPresent() && - productAnalysis?.needsAnalysis == true && - appStore.state.shoppingState.productsInAnalysis.contains(productPageUrl) + private fun ProductReviewState.isAnalysisPresentOrNoAnalysisPresent() = + this is ProductReviewState.AnalysisPresent || this is ProductReviewState.NoAnalysisPresent + + private suspend fun Store.updateRecommendedProductState() { + val currentState = state + if (currentState is ReviewQualityCheckState.OptedIn && + (currentState.productRecommendationsExposure || (currentState.productRecommendationsPreference == true)) ) { - dispatch(ReviewQualityCheckAction.RestoreReanalysis) + val productRecommendation = reviewQualityCheckService.productRecommendation( + currentState.productRecommendationsPreference ?: false, + ) + if (currentState.productRecommendationsPreference == true) { + productRecommendation.toRecommendedProductState().also { + dispatch(UpdateRecommendedProduct(it)) + } + } } } - private fun ProductReviewState.isAnalysisPresentOrNoAnalysisPresent() = - this is ProductReviewState.AnalysisPresent || this is ProductReviewState.NoAnalysisPresent + private fun AnalysisStatusDto?.isPendingOrInProgress(): Boolean = + this == AnalysisStatusDto.PENDING || this == AnalysisStatusDto.IN_PROGRESS } diff --git a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckPreferences.kt b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckPreferences.kt index d1fbf9925d..af9d82b303 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckPreferences.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckPreferences.kt @@ -44,7 +44,7 @@ interface ReviewQualityCheckPreferences { * Implementation of [ReviewQualityCheckPreferences] that uses [Settings] to store/fetch * preferences. * - * @property settings The [Settings] instance to use. + * @param settings The [Settings] instance to use. */ class DefaultReviewQualityCheckPreferences( private val settings: Settings, diff --git a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckPreferencesMiddleware.kt b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckPreferencesMiddleware.kt index 424a09a638..825b1ad231 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckPreferencesMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckPreferencesMiddleware.kt @@ -8,22 +8,36 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import mozilla.components.lib.state.MiddlewareContext import mozilla.components.lib.state.Store +import org.mozilla.fenix.components.AppStore +import org.mozilla.fenix.components.appstate.AppAction +import org.mozilla.fenix.components.appstate.AppAction.ShoppingAction +import org.mozilla.fenix.components.appstate.AppAction.ShoppingAction.HighlightsCardExpanded +import org.mozilla.fenix.components.appstate.AppAction.ShoppingAction.InfoCardExpanded +import org.mozilla.fenix.components.appstate.AppAction.ShoppingAction.SettingsCardExpanded +import org.mozilla.fenix.components.appstate.AppState +import org.mozilla.fenix.components.appstate.shopping.ShoppingState.CardState +import org.mozilla.fenix.shopping.ShoppingExperienceFeature import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction import org.mozilla.fenix.shopping.store.ReviewQualityCheckMiddleware import org.mozilla.fenix.shopping.store.ReviewQualityCheckState +import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn /** * Middleware for getting and setting review quality check user preferences. * - * @property reviewQualityCheckPreferences The [ReviewQualityCheckPreferences] instance to get and + * @param reviewQualityCheckPreferences The [ReviewQualityCheckPreferences] instance to get and * set preferences for the review quality check feature. - * @property reviewQualityCheckVendorsService The [ReviewQualityCheckVendorsService] instance for + * @param reviewQualityCheckVendorsService The [ReviewQualityCheckVendorsService] instance for * getting the list of product vendors. - * @property scope The [CoroutineScope] to use for launching coroutines. + * @param appStore The [AppStore] instance for dispatching [ShoppingAction]s. + * @param shoppingExperienceFeature The [ShoppingExperienceFeature] instance to get feature flags. + * @param scope The [CoroutineScope] to use for launching coroutines. */ class ReviewQualityCheckPreferencesMiddleware( private val reviewQualityCheckPreferences: ReviewQualityCheckPreferences, private val reviewQualityCheckVendorsService: ReviewQualityCheckVendorsService, + private val appStore: AppStore, + private val shoppingExperienceFeature: ShoppingExperienceFeature, private val scope: CoroutineScope, ) : ReviewQualityCheckMiddleware { @@ -44,6 +58,7 @@ class ReviewQualityCheckPreferencesMiddleware( } } + @Suppress("LongMethod") private fun processAction( store: Store, action: ReviewQualityCheckAction.PreferencesMiddlewareAction, @@ -56,9 +71,19 @@ class ReviewQualityCheckPreferencesMiddleware( reviewQualityCheckPreferences.productRecommendationsEnabled() val updateUserPreferences = if (hasUserOptedIn) { + val savedCardState = + reviewQualityCheckVendorsService.selectedTabUrl()?.let { + appStore.state.shoppingState.productCardState.getOrElse(it) { CardState() } + } ?: CardState() + ReviewQualityCheckAction.OptInCompleted( isProductRecommendationsEnabled = isProductRecommendationsEnabled, + productRecommendationsExposure = + shoppingExperienceFeature.isProductRecommendationsExposureEnabled, productVendor = reviewQualityCheckVendorsService.productVendor(), + isHighlightsExpanded = savedCardState.isHighlightsExpanded, + isInfoExpanded = savedCardState.isInfoExpanded, + isSettingsExpanded = savedCardState.isSettingsExpanded, ) } else { val productVendors = reviewQualityCheckVendorsService.productVendors() @@ -75,7 +100,12 @@ class ReviewQualityCheckPreferencesMiddleware( store.dispatch( ReviewQualityCheckAction.OptInCompleted( isProductRecommendationsEnabled = isProductRecommendationsEnabled, + productRecommendationsExposure = + shoppingExperienceFeature.isProductRecommendationsExposureEnabled, productVendor = reviewQualityCheckVendorsService.productVendor(), + isHighlightsExpanded = false, + isInfoExpanded = false, + isSettingsExpanded = false, ), ) @@ -103,6 +133,53 @@ class ReviewQualityCheckPreferencesMiddleware( } } } + + ReviewQualityCheckAction.ExpandCollapseHighlights -> { + appStore.dispatchShoppingAction( + reviewQualityCheckState = store.state, + action = { productPageUrl, optedIn -> + HighlightsCardExpanded( + productPageUrl = productPageUrl, + expanded = optedIn.isHighlightsExpanded, + ) + }, + ) + } + + ReviewQualityCheckAction.ExpandCollapseInfo -> { + appStore.dispatchShoppingAction( + reviewQualityCheckState = store.state, + action = { productPageUrl, optedIn -> + InfoCardExpanded( + productPageUrl = productPageUrl, + expanded = optedIn.isInfoExpanded, + ) + }, + ) + } + + ReviewQualityCheckAction.ExpandCollapseSettings -> { + appStore.dispatchShoppingAction( + reviewQualityCheckState = store.state, + action = { productPageUrl, optedIn -> + SettingsCardExpanded( + productPageUrl = productPageUrl, + expanded = optedIn.isSettingsExpanded, + ) + }, + ) + } + } + } + + private fun Store.dispatchShoppingAction( + reviewQualityCheckState: ReviewQualityCheckState, + action: (productPageUrl: String, optedIn: OptedIn) -> ShoppingAction, + ) { + if (reviewQualityCheckState is OptedIn) { + reviewQualityCheckVendorsService.selectedTabUrl()?.let { + dispatch(action(it, reviewQualityCheckState)) + } } } } diff --git a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckService.kt b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckService.kt index d6e3b070e5..27c538d496 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckService.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckService.kt @@ -9,7 +9,9 @@ import kotlinx.coroutines.withContext import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.shopping.ProductAnalysis +import mozilla.components.concept.engine.shopping.ProductRecommendation import mozilla.components.support.base.log.logger.Logger +import org.mozilla.fenix.GleanMetrics.Shopping import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -40,15 +42,27 @@ interface ReviewQualityCheckService { suspend fun analysisStatus(): AnalysisStatusDto? /** - * Returns the selected tab url. + * Fetches product recommendations related to the product user is browsing in the current tab. + * + * @return [ProductRecommendation] if request succeeds, null otherwise. + */ + suspend fun productRecommendation(shouldRecordAvailableTelemetry: Boolean): ProductRecommendation? + + /** + * Sends a click attribution event for a given product aid. + */ + suspend fun recordRecommendedProductClick(productAid: String) + + /** + * Sends an impression attribution event for a given product aid. */ - fun selectedTabUrl(): String? + suspend fun recordRecommendedProductImpression(productAid: String) } /** * Service that handles the network requests for the review quality check feature. * - * @property browserStore Reference to the application's [BrowserStore] to access state. + * @param browserStore Reference to the application's [BrowserStore] to access state. */ class DefaultReviewQualityCheckService( private val browserStore: BrowserStore, @@ -107,11 +121,65 @@ class DefaultReviewQualityCheckService( } } - override fun selectedTabUrl(): String? = - browserStore.state.selectedTab?.content?.url + override suspend fun productRecommendation(shouldRecordAvailableTelemetry: Boolean): ProductRecommendation? = + withContext(Dispatchers.Main) { + suspendCoroutine { continuation -> + browserStore.state.selectedTab?.let { tab -> + tab.engineState.engineSession?.requestProductRecommendations( + url = tab.content.url, + onResult = { + if (it.isEmpty()) { + if (shouldRecordAvailableTelemetry) { + Shopping.surfaceNoAdsAvailable.record() + } + } else { + Shopping.adsExposure.record() + } + // Return the first available recommendation since ui requires only + // one recommendation. + continuation.resume(it.firstOrNull()) + }, + onException = { + logger.error("Error fetching product recommendation", it) + continuation.resume(null) + }, + ) + } + } + } - private inline fun > String.asEnumOrDefault(defaultValue: T? = null): T? = - enumValues().firstOrNull { it.name.equals(this, ignoreCase = true) } ?: defaultValue + override suspend fun recordRecommendedProductClick(productAid: String) = + withContext(Dispatchers.Main) { + suspendCoroutine { continuation -> + browserStore.state.selectedTab?.engineState?.engineSession?.sendClickAttributionEvent( + aid = productAid, + onResult = { + continuation.resume(Unit) + }, + onException = { + logger.error("Error sending click attribution event", it) + continuation.resume(Unit) + }, + ) + } + } + + override suspend fun recordRecommendedProductImpression(productAid: String) { + withContext(Dispatchers.Main) { + suspendCoroutine { continuation -> + browserStore.state.selectedTab?.engineState?.engineSession?.sendImpressionAttributionEvent( + aid = productAid, + onResult = { + continuation.resume(Unit) + }, + onException = { + logger.error("Error sending impression attribution event", it) + continuation.resume(Unit) + }, + ) + } + } + } } /** diff --git a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckTelemetryMiddleware.kt b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckTelemetryMiddleware.kt index c77a31a755..fcad142752 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckTelemetryMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckTelemetryMiddleware.kt @@ -5,11 +5,16 @@ package org.mozilla.fenix.shopping.middleware import mozilla.components.lib.state.MiddlewareContext +import mozilla.components.lib.state.Store import org.mozilla.fenix.GleanMetrics.Shopping import org.mozilla.fenix.GleanMetrics.ShoppingSettings import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction import org.mozilla.fenix.shopping.store.ReviewQualityCheckMiddleware import org.mozilla.fenix.shopping.store.ReviewQualityCheckState +import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent.AnalysisStatus + +private const val ACTION_ENABLED = "enabled" +private const val ACTION_DISABLED = "disabled" /** * Middleware that captures telemetry events for the review quality check feature. @@ -24,86 +29,139 @@ class ReviewQualityCheckTelemetryMiddleware : ReviewQualityCheckMiddleware { next(action) when (action) { - is ReviewQualityCheckAction.TelemetryAction -> processAction(action) + is ReviewQualityCheckAction.TelemetryAction -> processAction(context.store, action) else -> { // no-op } } } + @Suppress("LongMethod") private fun processAction( + store: Store, action: ReviewQualityCheckAction.TelemetryAction, - ) = when (action) { - is ReviewQualityCheckAction.OptIn -> { - Shopping.surfaceOptInAccepted.record() - ShoppingSettings.userHasOnboarded.set(true) - } + ) { + when (action) { + is ReviewQualityCheckAction.OptIn -> { + Shopping.surfaceOptInAccepted.record() + ShoppingSettings.userHasOnboarded.set(true) + } - is ReviewQualityCheckAction.OptOut -> { - ShoppingSettings.componentOptedOut.set(true) - } + is ReviewQualityCheckAction.OptOut -> { + ShoppingSettings.componentOptedOut.set(true) + } - is ReviewQualityCheckAction.BottomSheetClosed -> { - Shopping.surfaceClosed.record( - Shopping.SurfaceClosedExtra(action.source.sourceName), - ) - } + is ReviewQualityCheckAction.BottomSheetClosed -> { + Shopping.surfaceClosed.record( + Shopping.SurfaceClosedExtra(action.source.sourceName), + ) + } - is ReviewQualityCheckAction.BottomSheetDisplayed -> { - Shopping.surfaceDisplayed.record( - Shopping.SurfaceDisplayedExtra(action.view.state), - ) - } + is ReviewQualityCheckAction.BottomSheetDisplayed -> { + Shopping.surfaceDisplayed.record( + Shopping.SurfaceDisplayedExtra(action.view.state), + ) + } - is ReviewQualityCheckAction.OpenExplainerLearnMoreLink -> { - Shopping.surfaceReviewQualityExplainerUrlClicked.record() - } + is ReviewQualityCheckAction.OpenExplainerLearnMoreLink -> { + Shopping.surfaceReviewQualityExplainerUrlClicked.record() + } - is ReviewQualityCheckAction.OpenOnboardingLearnMoreLink -> { - Shopping.surfaceLearnMoreClicked.record() - } + is ReviewQualityCheckAction.OpenOnboardingLearnMoreLink -> { + Shopping.surfaceLearnMoreClicked.record() + } - is ReviewQualityCheckAction.OpenOnboardingPrivacyPolicyLink -> { - Shopping.surfaceShowPrivacyPolicyClicked.record() - } + is ReviewQualityCheckAction.OpenOnboardingPrivacyPolicyLink -> { + Shopping.surfaceShowPrivacyPolicyClicked.record() + } - is ReviewQualityCheckAction.OpenOnboardingTermsLink -> { - Shopping.surfaceShowTermsClicked.record() - } + is ReviewQualityCheckAction.OpenOnboardingTermsLink -> { + Shopping.surfaceShowTermsClicked.record() + } - is ReviewQualityCheckAction.NotNowClicked -> { - Shopping.surfaceNotNowClicked.record() - } + is ReviewQualityCheckAction.NotNowClicked -> { + Shopping.surfaceNotNowClicked.record() + } - is ReviewQualityCheckAction.ShowMoreRecentReviewsClicked -> { - Shopping.surfaceShowMoreRecentReviewsClicked.record() - } + is ReviewQualityCheckAction.ExpandCollapseHighlights -> { + val state = store.state + if (state is ReviewQualityCheckState.OptedIn && state.isHighlightsExpanded) { + Shopping.surfaceShowMoreRecentReviewsClicked.record() + } + } - is ReviewQualityCheckAction.ExpandSettingsClicked -> { - Shopping.surfaceExpandSettings.record() - } - is ReviewQualityCheckAction.NoAnalysisDisplayed -> { - Shopping.surfaceNoReviewReliabilityAvailable.record() - } + is ReviewQualityCheckAction.ExpandCollapseSettings -> { + val state = store.state + if (state is ReviewQualityCheckState.OptedIn && state.isSettingsExpanded) { + Shopping.surfaceExpandSettings.record() + } + } - is ReviewQualityCheckAction.AnalyzeProduct -> { - Shopping.surfaceAnalyzeReviewsNoneAvailableClicked.record() - } + is ReviewQualityCheckAction.NoAnalysisDisplayed -> { + Shopping.surfaceNoReviewReliabilityAvailable.record() + } - is ReviewQualityCheckAction.ReanalyzeProduct -> { - Shopping.surfaceReanalyzeClicked.record() - } + is ReviewQualityCheckAction.UpdateProductReview -> { + val state = store.state + if (state.isStaleAnalysis() && !action.restoreAnalysis) { + Shopping.surfaceStaleAnalysisShown.record() + } + } - is ReviewQualityCheckAction.ReportProductBackInStock -> { - Shopping.surfaceReactivatedButtonClicked.record() - } + is ReviewQualityCheckAction.AnalyzeProduct -> { + Shopping.surfaceAnalyzeReviewsNoneAvailableClicked.record() + } - is ReviewQualityCheckAction.OptOutCompleted -> { - Shopping.surfaceOnboardingDisplayed.record() + is ReviewQualityCheckAction.ReanalyzeProduct -> { + Shopping.surfaceReanalyzeClicked.record() + } + + is ReviewQualityCheckAction.ReportProductBackInStock -> { + Shopping.surfaceReactivatedButtonClicked.record() + } + + is ReviewQualityCheckAction.OptOutCompleted -> { + Shopping.surfaceOnboardingDisplayed.record() + } + + is ReviewQualityCheckAction.OpenPoweredByLink -> { + Shopping.surfacePoweredByFakespotLinkClicked.record() + } + + is ReviewQualityCheckAction.RecommendedProductImpression -> { + Shopping.surfaceAdsImpression.record() + } + + is ReviewQualityCheckAction.RecommendedProductClick -> { + Shopping.surfaceAdsClicked.record() + } + + ReviewQualityCheckAction.ToggleProductRecommendation -> { + val state = store.state + if (state is ReviewQualityCheckState.OptedIn && + state.productRecommendationsPreference != null + ) { + val toggleAction = if (state.productRecommendationsPreference) { + ACTION_ENABLED + } else { + ACTION_DISABLED + } + Shopping.surfaceAdsSettingToggled.record( + Shopping.SurfaceAdsSettingToggledExtra( + action = toggleAction, + ), + ) + } + } } is ReviewQualityCheckAction.OpenPoweredByLink -> { Shopping.surfacePoweredByFakespotLinkClicked.record() } } + + private fun ReviewQualityCheckState.isStaleAnalysis(): Boolean = + this is ReviewQualityCheckState.OptedIn && + this.productReviewState is ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent && + this.productReviewState.analysisStatus == AnalysisStatus.NEEDS_ANALYSIS } diff --git a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckVendorsService.kt b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckVendorsService.kt index 9f594d8584..e33697f54b 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckVendorsService.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckVendorsService.kt @@ -21,6 +21,11 @@ private val defaultVendorsList = enumValues().toList() */ interface ReviewQualityCheckVendorsService { + /** + * Returns the selected tab url. + */ + fun selectedTabUrl(): String? + /** * Returns the list of product vendors in order. */ @@ -31,14 +36,17 @@ interface ReviewQualityCheckVendorsService { * Default implementation of [ReviewQualityCheckVendorsService] that uses the [BrowserStore] to * identify the selected tab. * - * @property browserStore The [BrowserStore] instance to use. + * @param browserStore The [BrowserStore] instance to use. */ class DefaultReviewQualityCheckVendorsService( private val browserStore: BrowserStore, ) : ReviewQualityCheckVendorsService { + override fun selectedTabUrl(): String? = + browserStore.state.selectedTab?.content?.url + override fun productVendors(): List { - val selectedTabUrl = browserStore.state.selectedTab?.content?.url + val selectedTabUrl = selectedTabUrl() return if (selectedTabUrl == null) { defaultVendorsList diff --git a/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckAction.kt b/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckAction.kt index 43a8b34e4c..f40285df3f 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckAction.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckAction.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.shopping.store import mozilla.components.lib.state.Action import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState +import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.RecommendedProductState /** * Actions for review quality check feature. @@ -55,18 +56,26 @@ sealed interface ReviewQualityCheckAction : Action { /** * Triggered when the user has enabled or disabled product recommendations. */ - object ToggleProductRecommendation : PreferencesMiddlewareAction, UpdateAction + object ToggleProductRecommendation : PreferencesMiddlewareAction, UpdateAction, TelemetryAction /** * Triggered as a result of a [OptIn] or [Init] whe user has opted in for shopping experience. * * @property isProductRecommendationsEnabled Reflects the user preference update to display * recommended product. Null when product recommendations feature is disabled. + * @property productRecommendationsExposure Whether product recommendations exposure is enabled. * @property productVendor The vendor of the product. + * @property isHighlightsExpanded Whether the highlights card should be expanded. + * @property isInfoExpanded Whether the info card should be expanded. + * @property isSettingsExpanded Whether the settings card should be expanded. */ data class OptInCompleted( val isProductRecommendationsEnabled: Boolean?, + val productRecommendationsExposure: Boolean, val productVendor: ReviewQualityCheckState.ProductVendor, + val isHighlightsExpanded: Boolean, + val isInfoExpanded: Boolean, + val isSettingsExpanded: Boolean, ) : UpdateAction /** @@ -79,9 +88,22 @@ sealed interface ReviewQualityCheckAction : Action { ) : UpdateAction, TelemetryAction /** - * Triggered as a result of a [NetworkAction] to update the state. + * Triggered as a result of a [NetworkAction] to update the [ProductReviewState]. + * + * @property productReviewState The product review state to update. + * @property restoreAnalysis Signals whether the analysis will be restored right after the update. */ - data class UpdateProductReview(val productReviewState: ProductReviewState) : UpdateAction + data class UpdateProductReview( + val productReviewState: ProductReviewState, + val restoreAnalysis: Boolean, + ) : UpdateAction, TelemetryAction + + /** + * Triggered as a result of a [NetworkAction] to update the [RecommendedProductState]. + */ + data class UpdateRecommendedProduct( + val recommendedProductState: RecommendedProductState, + ) : UpdateAction /** * Triggered when the user has opted in to the review quality check feature and the UI is opened. @@ -109,6 +131,26 @@ sealed interface ReviewQualityCheckAction : Action { */ object AnalyzeProduct : NetworkAction, UpdateAction, TelemetryAction + /** + * Triggered when the user clicks on the recommended product. + * + * @property productAid The product's aid. + * @property productUrl The product's link to open. + */ + data class RecommendedProductClick( + val productAid: String, + val productUrl: String, + ) : NavigationMiddlewareAction, NetworkAction, TelemetryAction + + /** + * Triggered when the user views the recommended product. + * + * @property productAid The product's aid. + */ + data class RecommendedProductImpression( + val productAid: String, + ) : NetworkAction, TelemetryAction + /** * Triggered when the user clicks on learn more link on the explainer card. */ @@ -156,12 +198,17 @@ sealed interface ReviewQualityCheckAction : Action { /** * Triggered when the user expands the recent reviews card. */ - object ShowMoreRecentReviewsClicked : TelemetryAction + object ExpandCollapseHighlights : TelemetryAction, UpdateAction, PreferencesMiddlewareAction + + /** + * Triggered when the user expands or collapses the settings card. + */ + object ExpandCollapseSettings : TelemetryAction, UpdateAction, PreferencesMiddlewareAction /** - * Triggered when the user expands the settings card. + * Triggered when the user expands or collapses the info card. */ - object ExpandSettingsClicked : TelemetryAction + object ExpandCollapseInfo : UpdateAction, PreferencesMiddlewareAction /** * Triggered when the No analysis card is displayed to the user. diff --git a/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckState.kt b/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckState.kt index 2a558de4c8..ce9a390d4d 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckState.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckState.kt @@ -4,9 +4,8 @@ package org.mozilla.fenix.shopping.store +import androidx.compose.runtime.Immutable import mozilla.components.lib.state.State -import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.HighlightType -import java.util.SortedMap private const val NUMBER_OF_HIGHLIGHTS_FOR_COMPACT_MODE = 2 @@ -42,12 +41,20 @@ sealed interface ReviewQualityCheckState : State { * @property productRecommendationsPreference User preference whether to show product * recommendations. True if product recommendations should be shown. Null indicates that product * recommendations are disabled. + * @property productRecommendationsExposure Whether product recommendations exposure is enabled. * @property productVendor The vendor of the product. + * @property isSettingsExpanded Whether or not the settings card is expanded. + * @property isInfoExpanded Whether or not the info card is expanded. + * @property isHighlightsExpanded Whether or not the highlights card is expanded. */ data class OptedIn( val productReviewState: ProductReviewState = ProductReviewState.Loading, val productRecommendationsPreference: Boolean?, + val productRecommendationsExposure: Boolean, val productVendor: ProductVendor, + val isSettingsExpanded: Boolean = false, + val isInfoExpanded: Boolean = false, + val isHighlightsExpanded: Boolean = false, ) : ReviewQualityCheckState { /** @@ -99,7 +106,7 @@ sealed interface ReviewQualityCheckState : State { * @property analysisStatus The status of the product analysis. * @property adjustedRating The adjusted rating taking review quality into consideration. * @property productUrl The url of the product the user is browsing. - * @property highlights Optional highlights based on recent reviews of the product. + * @property highlightsInfo Optional highlights based on recent reviews of the product. * @property recommendedProductState The state of the recommended product. */ data class AnalysisPresent( @@ -108,22 +115,42 @@ sealed interface ReviewQualityCheckState : State { val analysisStatus: AnalysisStatus, val adjustedRating: Float?, val productUrl: String, - val highlights: SortedMap>?, + val highlightsInfo: HighlightsInfo?, val recommendedProductState: RecommendedProductState = RecommendedProductState.Initial, ) : ProductReviewState { init { - require(!(highlights == null && reviewGrade == null && adjustedRating == null)) { + require(!(highlightsInfo == null && reviewGrade == null && adjustedRating == null)) { "AnalysisPresent state should only be created when at least one of " + "reviewGrade, adjustedRating or highlights is not null" } } - val showMoreButtonVisible: Boolean = - highlights != null && highlights != highlights.forCompactMode() - - val highlightsFadeVisible: Boolean = - highlights != null && showMoreButtonVisible && - highlights.forCompactMode().entries.first().value.size > 1 + /** + * Container for highlights and it's derived properties + * + * @property highlights highlights based on recent reviews of the product. + */ + @Immutable + data class HighlightsInfo( + val highlights: Map>, + ) { + + /** + * Highlights to display in compact mode that contains first 2 highlights of the + * first highlight type. + */ + val highlightsForCompactMode: Map> = + highlights.entries.first().let { entry -> + mapOf( + entry.key to entry.value.take(NUMBER_OF_HIGHLIGHTS_FOR_COMPACT_MODE), + ) + } + + val showMoreButtonVisible: Boolean = highlights != highlightsForCompactMode + + val highlightsFadeVisible: Boolean = + showMoreButtonVisible && highlightsForCompactMode.entries.first().value.size > 1 + } /** * The status of the product analysis. @@ -158,19 +185,10 @@ sealed interface ReviewQualityCheckState : State { */ object Initial : RecommendedProductState - /** - * The state when the recommended product is loading. - */ - object Loading : RecommendedProductState - - /** - * The state when an error has occurred while fetching the recommended product. - */ - object Error : RecommendedProductState - /** * The state when the recommended product is available. * + * @property aid The unique identifier of the product. * @property name The name of the product. * @property productUrl The url of the product. * @property imageUrl The url of the image of the product. @@ -181,6 +199,7 @@ sealed interface ReviewQualityCheckState : State { * @property analysisUrl The url of the analysis of the product. */ data class Product( + val aid: String, val name: String, val productUrl: String, val imageUrl: String, @@ -203,12 +222,3 @@ sealed interface ReviewQualityCheckState : State { this } } - -/** - * Highlights to display in compact mode that contains first 2 highlights of the first - * highlight type. - */ -fun Map>.forCompactMode(): Map> = - entries.first().let { entry -> - mapOf(entry.key to entry.value.take(NUMBER_OF_HIGHLIGHTS_FOR_COMPACT_MODE)) - } diff --git a/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStore.kt b/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStore.kt index 35f319b7e4..e0c598b959 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStore.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStore.kt @@ -11,12 +11,14 @@ import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductR /** * Store for review quality check feature. * + * @param initialState The initial state of the store. * @param middleware The list of middlewares to use. */ class ReviewQualityCheckStore( + initialState: ReviewQualityCheckState = ReviewQualityCheckState.Initial, middleware: List, ) : Store( - initialState = ReviewQualityCheckState.Initial, + initialState = initialState, middleware = middleware, reducer = ::reducer, ) { @@ -36,6 +38,7 @@ private fun reducer( return state } +@Suppress("LongMethod") private fun mapStateForUpdateAction( state: ReviewQualityCheckState, action: ReviewQualityCheckAction.UpdateAction, @@ -43,14 +46,25 @@ private fun mapStateForUpdateAction( return when (action) { is ReviewQualityCheckAction.OptInCompleted -> { if (state is ReviewQualityCheckState.OptedIn) { - state.copy(productRecommendationsPreference = action.isProductRecommendationsEnabled) + state.copy( + productRecommendationsPreference = action.isProductRecommendationsEnabled, + productRecommendationsExposure = action.productRecommendationsExposure, + isHighlightsExpanded = action.isHighlightsExpanded, + isInfoExpanded = action.isInfoExpanded, + isSettingsExpanded = action.isSettingsExpanded, + ) } else { ReviewQualityCheckState.OptedIn( productRecommendationsPreference = action.isProductRecommendationsEnabled, + productRecommendationsExposure = action.productRecommendationsExposure, productVendor = action.productVendor, + isHighlightsExpanded = action.isHighlightsExpanded, + isInfoExpanded = action.isInfoExpanded, + isSettingsExpanded = action.isSettingsExpanded, ) } } + is ReviewQualityCheckAction.OptOutCompleted -> { ReviewQualityCheckState.NotOptedIn(action.productVendors) } @@ -59,9 +73,39 @@ private fun mapStateForUpdateAction( ReviewQualityCheckState.NotOptedIn() } + ReviewQualityCheckAction.ExpandCollapseSettings -> { + state.mapIfOptedIn { + it.copy(isSettingsExpanded = !it.isSettingsExpanded) + } + } + + ReviewQualityCheckAction.ExpandCollapseInfo -> { + state.mapIfOptedIn { + it.copy(isInfoExpanded = !it.isInfoExpanded) + } + } + + ReviewQualityCheckAction.ExpandCollapseHighlights -> { + state.mapIfOptedIn { + it.copy(isHighlightsExpanded = !it.isHighlightsExpanded) + } + } + ReviewQualityCheckAction.ToggleProductRecommendation -> { if (state is ReviewQualityCheckState.OptedIn && state.productRecommendationsPreference != null) { - state.copy(productRecommendationsPreference = !state.productRecommendationsPreference) + if (state.productReviewState is ProductReviewState.AnalysisPresent && + state.productRecommendationsPreference + ) { + // Removes any existing product recommendation from UI + state.copy( + productRecommendationsPreference = false, + productReviewState = state.productReviewState.copy( + recommendedProductState = ReviewQualityCheckState.RecommendedProductState.Initial, + ), + ) + } else { + state.copy(productRecommendationsPreference = !state.productRecommendationsPreference) + } } else { state } @@ -101,5 +145,21 @@ private fun mapStateForUpdateAction( } } } + + is ReviewQualityCheckAction.UpdateRecommendedProduct -> { + state.mapIfOptedIn { + if (it.productReviewState is ProductReviewState.AnalysisPresent && + it.productRecommendationsPreference == true + ) { + it.copy( + productReviewState = it.productReviewState.copy( + recommendedProductState = action.recommendedProductState, + ), + ) + } else { + it + } + } + } } } diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ui/NoAnalysis.kt b/app/src/main/java/org/mozilla/fenix/shopping/ui/NoAnalysis.kt index efb08660d1..c0a3821adb 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/ui/NoAnalysis.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/ui/NoAnalysis.kt @@ -41,12 +41,16 @@ import org.mozilla.fenix.theme.FirefoxTheme * @param isAnalyzing Whether or not the displayed product is being analyzed. * @param productRecommendationsEnabled The current state of the product recommendations toggle. * @param productVendor The vendor of the product. + * @param isSettingsExpanded Whether or not the settings card is expanded. + * @param isInfoExpanded Whether or not the info card is expanded. * @param onAnalyzeClick Invoked when the user clicks on the check review button. * @param onReviewGradeLearnMoreClick Invoked when the user clicks to learn more about review grades. * @param onOptOutClick Invoked when the user opts out of the review quality check feature. * @param onProductRecommendationsEnabledStateChange Invoked when the user changes the product * recommendations toggle state. - * @param onExpandSettings Invoked when the user expands the settings card. + * @param onSettingsExpandToggleClick Invoked when the user expands or collapses the settings card. + * @param onInfoExpandToggleClick Invoked when the user expands or collapses the info card. + * @param onFooterLinkClick Invoked when the user clicks on the footer link. * @param modifier Modifier to be applied to the composable. */ @Suppress("LongParameterList") @@ -55,11 +59,15 @@ fun NoAnalysis( isAnalyzing: Boolean, productRecommendationsEnabled: Boolean?, productVendor: ReviewQualityCheckState.ProductVendor, + isSettingsExpanded: Boolean, + isInfoExpanded: Boolean, onAnalyzeClick: () -> Unit, onReviewGradeLearnMoreClick: () -> Unit, onOptOutClick: () -> Unit, onProductRecommendationsEnabledStateChange: (Boolean) -> Unit, - onExpandSettings: () -> Unit, + onSettingsExpandToggleClick: () -> Unit, + onInfoExpandToggleClick: () -> Unit, + onFooterLinkClick: () -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -70,16 +78,23 @@ fun NoAnalysis( ReviewQualityInfoCard( productVendor = productVendor, + isExpanded = isInfoExpanded, onLearnMoreClick = onReviewGradeLearnMoreClick, + onExpandToggleClick = onInfoExpandToggleClick, ) ReviewQualityCheckSettingsCard( productRecommendationsEnabled = productRecommendationsEnabled, + isExpanded = isSettingsExpanded, onProductRecommendationsEnabledStateChange = onProductRecommendationsEnabledStateChange, onTurnOffReviewQualityCheckClick = onOptOutClick, - onExpandSettings = onExpandSettings, + onExpandToggleClick = onSettingsExpandToggleClick, modifier = Modifier.fillMaxWidth(), ) + + ReviewQualityCheckFooter( + onLinkClick = onFooterLinkClick, + ) } } @@ -169,8 +184,6 @@ private fun ReviewQualityNoAnalysisCard( @Composable @LightDarkPreview private fun NoAnalysisPreview() { - var isAnalyzing by remember { mutableStateOf(false) } - FirefoxTheme { Box( modifier = Modifier @@ -178,15 +191,24 @@ private fun NoAnalysisPreview() { .background(color = FirefoxTheme.colors.layer1) .padding(all = 16.dp), ) { + var isAnalyzing by remember { mutableStateOf(false) } + var productRecommendationsEnabled by remember { mutableStateOf(false) } + var isSettingsExpanded by remember { mutableStateOf(false) } + var isInfoExpanded by remember { mutableStateOf(false) } + NoAnalysis( isAnalyzing = isAnalyzing, - productVendor = ReviewQualityCheckState.ProductVendor.AMAZON, onAnalyzeClick = { isAnalyzing = !isAnalyzing }, - productRecommendationsEnabled = false, + productVendor = ReviewQualityCheckState.ProductVendor.AMAZON, + productRecommendationsEnabled = productRecommendationsEnabled, + isSettingsExpanded = isSettingsExpanded, + isInfoExpanded = isInfoExpanded, onReviewGradeLearnMoreClick = {}, onOptOutClick = {}, - onProductRecommendationsEnabledStateChange = {}, - onExpandSettings = {}, + onProductRecommendationsEnabledStateChange = { productRecommendationsEnabled = it }, + onSettingsExpandToggleClick = { isSettingsExpanded = !isSettingsExpanded }, + onInfoExpandToggleClick = { isInfoExpanded = !isInfoExpanded }, + onFooterLinkClick = {}, modifier = Modifier.fillMaxWidth(), ) } diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ui/ProductAnalysis.kt b/app/src/main/java/org/mozilla/fenix/shopping/ui/ProductAnalysis.kt index c99280e2f4..9c8af4d471 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/ui/ProductAnalysis.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/ui/ProductAnalysis.kt @@ -8,6 +8,7 @@ import androidx.compose.animation.Crossfade import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.spring import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -18,6 +19,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.Icon import androidx.compose.material.Text @@ -33,22 +35,29 @@ import androidx.compose.ui.layout.layout import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import org.mozilla.fenix.R import org.mozilla.fenix.compose.Divider +import org.mozilla.fenix.compose.Image import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.compose.button.SecondaryButton +import org.mozilla.fenix.compose.ext.onShown import org.mozilla.fenix.shopping.store.ReviewQualityCheckState import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.HighlightType import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent.AnalysisStatus -import org.mozilla.fenix.shopping.store.forCompactMode +import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductReviewState.AnalysisPresent.HighlightsInfo +import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.RecommendedProductState import org.mozilla.fenix.theme.FirefoxTheme -import java.util.SortedMap private val combinedParentHorizontalPadding = 32.dp +private val productRecommendationImageSize = 60.dp + +private const val PRODUCT_RECOMMENDATION_SETTLE_TIME_MS = 1500 +private const val PRODUCT_RECOMMENDATION_IMPRESSION_THRESHOLD = 0.5f /** * UI for review quality check content displaying product analysis. @@ -56,14 +65,20 @@ private val combinedParentHorizontalPadding = 32.dp * @param productRecommendationsEnabled The current state of the product recommendations toggle. * @param productAnalysis The product analysis to display. * @param productVendor The vendor of the product. + * @param isSettingsExpanded Whether or not the settings card is expanded. + * @param isInfoExpanded Whether or not the info card is expanded. + * @param isHighlightsExpanded Whether or not the highlights card is expanded. * @param onOptOutClick Invoked when the user opts out of the review quality check feature. * @param onReanalyzeClick Invoked when the user clicks to re-analyze a product. * @param onProductRecommendationsEnabledStateChange Invoked when the user changes the product * recommendations toggle state. * @param onReviewGradeLearnMoreClick Invoked when the user clicks to learn more about review grades. * @param onFooterLinkClick Invoked when the user clicks on the footer link. - * @param onShowMoreRecentReviewsClicked Invoked when the user clicks to show more recent reviews. - * @param onExpandSettings Invoked when the user expands the settings card. + * @param onHighlightsExpandToggleClick Invoked when the user clicks to show more recent reviews. + * @param onSettingsExpandToggleClick Invoked when the user expands or collapses the settings card. + * @param onInfoExpandToggleClick Invoked when the user expands or collapses the info card. + * @param onRecommendedProductClick Invoked when the user clicks on the product recommendation. + * @param onRecommendedProductImpression Invoked when the user has seen the product recommendation. * @param modifier The modifier to be applied to the Composable. */ @Composable @@ -72,13 +87,19 @@ fun ProductAnalysis( productRecommendationsEnabled: Boolean?, productAnalysis: AnalysisPresent, productVendor: ReviewQualityCheckState.ProductVendor, + isSettingsExpanded: Boolean, + isInfoExpanded: Boolean, + isHighlightsExpanded: Boolean, onOptOutClick: () -> Unit, onReanalyzeClick: () -> Unit, onProductRecommendationsEnabledStateChange: (Boolean) -> Unit, onReviewGradeLearnMoreClick: () -> Unit, onFooterLinkClick: () -> Unit, - onShowMoreRecentReviewsClicked: () -> Unit, - onExpandSettings: () -> Unit, + onHighlightsExpandToggleClick: () -> Unit, + onSettingsExpandToggleClick: () -> Unit, + onInfoExpandToggleClick: () -> Unit, + onRecommendedProductClick: (aid: String, url: String) -> Unit, + onRecommendedProductImpression: (String) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -113,27 +134,37 @@ fun ProductAnalysis( ) } - if (productAnalysis.highlights != null) { + if (productAnalysis.highlightsInfo != null) { HighlightsCard( - highlights = productAnalysis.highlights, - highlightsFadeVisible = productAnalysis.highlightsFadeVisible, - showMoreButtonVisible = productAnalysis.showMoreButtonVisible, - onShowMoreRecentReviewsClicked = onShowMoreRecentReviewsClicked, + highlightsInfo = productAnalysis.highlightsInfo, + onHighlightsExpandToggleClick = onHighlightsExpandToggleClick, + isExpanded = isHighlightsExpanded, modifier = Modifier.fillMaxWidth(), ) } ReviewQualityInfoCard( productVendor = productVendor, + isExpanded = isInfoExpanded, modifier = Modifier.fillMaxWidth(), + onExpandToggleClick = onInfoExpandToggleClick, onLearnMoreClick = onReviewGradeLearnMoreClick, ) + if (productAnalysis.recommendedProductState is RecommendedProductState.Product) { + ProductRecommendation( + product = productAnalysis.recommendedProductState, + onClick = onRecommendedProductClick, + onImpression = onRecommendedProductImpression, + ) + } + ReviewQualityCheckSettingsCard( productRecommendationsEnabled = productRecommendationsEnabled, + isExpanded = isSettingsExpanded, onProductRecommendationsEnabledStateChange = onProductRecommendationsEnabledStateChange, onTurnOffReviewQualityCheckClick = onOptOutClick, - onExpandSettings = onExpandSettings, + onExpandToggleClick = onSettingsExpandToggleClick, modifier = Modifier.fillMaxWidth(), ) @@ -223,20 +254,17 @@ private fun AdjustedProductRatingCard( @Suppress("LongMethod") @Composable private fun HighlightsCard( - highlights: Map>, - highlightsFadeVisible: Boolean, - showMoreButtonVisible: Boolean, - onShowMoreRecentReviewsClicked: () -> Unit, + highlightsInfo: HighlightsInfo, + isExpanded: Boolean, + onHighlightsExpandToggleClick: () -> Unit, modifier: Modifier = Modifier, ) { ReviewQualityCheckCard(modifier = modifier) { - var isExpanded by remember { mutableStateOf(false) } - val highlightsForCompactMode = remember(highlights) { highlights.forCompactMode() } - val highlightsToDisplay = remember(isExpanded, highlights) { + val highlightsToDisplay = remember(isExpanded, highlightsInfo.highlights) { if (isExpanded) { - highlights + highlightsInfo.highlights } else { - highlightsForCompactMode + highlightsInfo.highlightsForCompactMode } } @@ -278,7 +306,7 @@ private fun HighlightsCard( targetState = isExpanded, label = "HighlightsCard-Crossfade", ) { expanded -> - if (expanded.not() && highlightsFadeVisible) { + if (expanded.not() && highlightsInfo.highlightsFadeVisible) { Spacer( modifier = Modifier .height(32.dp) @@ -296,7 +324,7 @@ private fun HighlightsCard( } } - if (showMoreButtonVisible) { + if (highlightsInfo.showMoreButtonVisible) { Spacer(modifier = Modifier.height(8.dp)) Divider(modifier = Modifier.extendWidthToParentBorder()) @@ -309,12 +337,7 @@ private fun HighlightsCard( } else { stringResource(R.string.review_quality_check_highlights_show_more) }, - onClick = { - if (!isExpanded) { - onShowMoreRecentReviewsClicked() - } - isExpanded = isExpanded.not() - }, + onClick = onHighlightsExpandToggleClick, ) } } @@ -404,6 +427,76 @@ private enum class Highlight( ), } +@Composable +private fun ProductRecommendation( + product: RecommendedProductState.Product, + onClick: (String, String) -> Unit, + onImpression: (String) -> Unit, +) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + ReviewQualityCheckCard( + modifier = Modifier + .fillMaxWidth() + .clickable { + onClick(product.aid, product.productUrl) + } + .onShown( + threshold = PRODUCT_RECOMMENDATION_IMPRESSION_THRESHOLD, + settleTime = PRODUCT_RECOMMENDATION_SETTLE_TIME_MS, + onVisible = { onImpression(product.aid) }, + ), + ) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = stringResource(R.string.review_quality_check_ad_title), + color = FirefoxTheme.colors.textPrimary, + style = FirefoxTheme.typography.headline8, + ) + + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Image( + url = product.imageUrl, + modifier = Modifier.size(productRecommendationImageSize), + targetSize = productRecommendationImageSize, + ) + + Text( + text = product.name, + modifier = Modifier.weight(1.0f), + color = FirefoxTheme.colors.textAccent, + textDecoration = TextDecoration.Underline, + style = FirefoxTheme.typography.body2, + ) + + ReviewGradeCompact(grade = product.reviewGrade) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = product.formattedPrice, + color = FirefoxTheme.colors.textPrimary, + style = FirefoxTheme.typography.headline8, + ) + + StarRating(value = product.adjustedRating) + } + } + } + + Text( + text = stringResource( + id = R.string.review_quality_check_ad_caption, + stringResource(id = R.string.shopping_product_name), + ), + color = FirefoxTheme.colors.textSecondary, + style = FirefoxTheme.typography.body2, + ) + } +} + private class ProductAnalysisPreviewModel( val productRecommendationsEnabled: Boolean?, val productAnalysis: AnalysisPresent, @@ -416,35 +509,36 @@ private class ProductAnalysisPreviewModel( analysisStatus: AnalysisStatus = AnalysisStatus.UP_TO_DATE, adjustedRating: Float? = 3.6f, productUrl: String = "", - highlights: SortedMap>? = sortedMapOf( - HighlightType.QUALITY to listOf( - "High quality", - "Excellent craftsmanship", - "Superior materials", - ), - HighlightType.PRICE to listOf( - "Affordable prices", - "Great value for money", - "Discounted offers", - ), - HighlightType.SHIPPING to listOf( - "Fast and reliable shipping", - "Free shipping options", - "Express delivery", - ), - HighlightType.PACKAGING_AND_APPEARANCE to listOf( - "Elegant packaging", - "Attractive appearance", - "Beautiful design", - ), - HighlightType.COMPETITIVENESS to listOf( - "Competitive pricing", - "Strong market presence", - "Unbeatable deals", + highlightsInfo: HighlightsInfo = HighlightsInfo( + mapOf( + HighlightType.QUALITY to listOf( + "High quality", + "Excellent craftsmanship", + "Superior materials", + ), + HighlightType.PRICE to listOf( + "Affordable prices", + "Great value for money", + "Discounted offers", + ), + HighlightType.SHIPPING to listOf( + "Fast and reliable shipping", + "Free shipping options", + "Express delivery", + ), + HighlightType.PACKAGING_AND_APPEARANCE to listOf( + "Elegant packaging", + "Attractive appearance", + "Beautiful design", + ), + HighlightType.COMPETITIVENESS to listOf( + "Competitive pricing", + "Strong market presence", + "Unbeatable deals", + ), ), ), - recommendedProductState: ReviewQualityCheckState.RecommendedProductState = - ReviewQualityCheckState.RecommendedProductState.Initial, + recommendedProductState: RecommendedProductState = RecommendedProductState.Initial, productVendor: ReviewQualityCheckState.ProductVendor = ReviewQualityCheckState.ProductVendor.AMAZON, ) : this( productRecommendationsEnabled = productRecommendationsEnabled, @@ -454,7 +548,7 @@ private class ProductAnalysisPreviewModel( analysisStatus = analysisStatus, adjustedRating = adjustedRating, productUrl = productUrl, - highlights = highlights, + highlightsInfo = highlightsInfo, recommendedProductState = recommendedProductState, ), productVendor = productVendor, @@ -476,13 +570,30 @@ private class ProductAnalysisPreviewModelParameterProvider : reviewGrade = null, ), ProductAnalysisPreviewModel( - highlights = sortedMapOf( - HighlightType.QUALITY to listOf( - "High quality", - "Excellent craftsmanship", + highlightsInfo = HighlightsInfo( + mapOf( + HighlightType.QUALITY to listOf( + "High quality", + "Excellent craftsmanship", + ), ), ), ), + ProductAnalysisPreviewModel( + productRecommendationsEnabled = true, + recommendedProductState = RecommendedProductState.Product( + aid = "aid", + name = "The best desk ever with a really really really long product name that " + + "forces the preview to wrap its text to at least 4 lines.", + productUrl = "www.mozilla.com", + imageUrl = "https://i.fakespot.io/b6vx27xf3rgwr1a597q6qd3rutp6", + formattedPrice = "$123.45", + reviewGrade = ReviewQualityCheckState.Grade.B, + adjustedRating = 4.23f, + isSponsored = true, + analysisUrl = "", + ), + ), ) } @@ -496,11 +607,17 @@ private fun ProductAnalysisPreview( onRequestDismiss = {}, ) { var productRecommendationsEnabled by remember { mutableStateOf(model.productRecommendationsEnabled) } + var isSettingsExpanded by remember { mutableStateOf(false) } + var isInfoExpanded by remember { mutableStateOf(false) } + var isHighlightsExpanded by remember { mutableStateOf(false) } ProductAnalysis( productRecommendationsEnabled = productRecommendationsEnabled, productAnalysis = model.productAnalysis, productVendor = model.productVendor, + isSettingsExpanded = isSettingsExpanded, + isInfoExpanded = isInfoExpanded, + isHighlightsExpanded = isHighlightsExpanded, onOptOutClick = {}, onReanalyzeClick = {}, onProductRecommendationsEnabledStateChange = { @@ -508,8 +625,11 @@ private fun ProductAnalysisPreview( }, onReviewGradeLearnMoreClick = {}, onFooterLinkClick = {}, - onShowMoreRecentReviewsClicked = {}, - onExpandSettings = {}, + onHighlightsExpandToggleClick = { isHighlightsExpanded = !isHighlightsExpanded }, + onSettingsExpandToggleClick = { isSettingsExpanded = !isSettingsExpanded }, + onInfoExpandToggleClick = { isInfoExpanded = !isInfoExpanded }, + onRecommendedProductClick = { _, _ -> }, + onRecommendedProductImpression = {}, ) } } diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ui/ProductAnalysisError.kt b/app/src/main/java/org/mozilla/fenix/shopping/ui/ProductAnalysisError.kt index cd36b01317..83e950c3e0 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/ui/ProductAnalysisError.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/ui/ProductAnalysisError.kt @@ -12,6 +12,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -27,12 +31,15 @@ import org.mozilla.fenix.theme.FirefoxTheme * @param error The error state to display. * @param productRecommendationsEnabled The current state of the product recommendations toggle. * @param productVendor The vendor of the product. + * @param isSettingsExpanded Whether or not the settings card is expanded. + * @param isInfoExpanded Whether or not the info card is expanded. * @param onReviewGradeLearnMoreClick Invoked when the user clicks to learn more about review grades. * @param onOptOutClick Invoked when the user opts out of the review quality check feature. * @param onProductRecommendationsEnabledStateChange Invoked when the user changes the product * recommendations toggle state. * @param onFooterLinkClick Invoked when the user clicks on the footer link. - * @param onExpandSettings Invoked when the user expands the settings card. + * @param onSettingsExpandToggleClick Invoked when the user expands or collapses the settings card. + * @param onInfoExpandToggleClick Invoked when the user expands or collapses the info card. * @param modifier Modifier to apply to the layout. */ @Composable @@ -41,11 +48,14 @@ fun ProductAnalysisError( error: ProductReviewState.Error, productRecommendationsEnabled: Boolean?, productVendor: ReviewQualityCheckState.ProductVendor, + isSettingsExpanded: Boolean, + isInfoExpanded: Boolean, onReviewGradeLearnMoreClick: () -> Unit, onOptOutClick: () -> Unit, onProductRecommendationsEnabledStateChange: (Boolean) -> Unit, onFooterLinkClick: () -> Unit, - onExpandSettings: () -> Unit, + onSettingsExpandToggleClick: () -> Unit, + onInfoExpandToggleClick: () -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -99,14 +109,17 @@ fun ProductAnalysisError( ReviewQualityInfoCard( productVendor = productVendor, + isExpanded = isInfoExpanded, onLearnMoreClick = onReviewGradeLearnMoreClick, + onExpandToggleClick = onInfoExpandToggleClick, ) ReviewQualityCheckSettingsCard( productRecommendationsEnabled = productRecommendationsEnabled, + isExpanded = isSettingsExpanded, onProductRecommendationsEnabledStateChange = onProductRecommendationsEnabledStateChange, onTurnOffReviewQualityCheckClick = onOptOutClick, - onExpandSettings = onExpandSettings, + onExpandToggleClick = onSettingsExpandToggleClick, modifier = Modifier.fillMaxWidth(), ) @@ -126,15 +139,22 @@ private fun ProductAnalysisErrorPreview() { .background(color = FirefoxTheme.colors.layer1) .padding(all = 16.dp), ) { + var productRecommendationsEnabled by remember { mutableStateOf(false) } + var isSettingsExpanded by remember { mutableStateOf(false) } + var isInfoExpanded by remember { mutableStateOf(false) } + ProductAnalysisError( error = ProductReviewState.Error.NetworkError, - productRecommendationsEnabled = true, + productRecommendationsEnabled = productRecommendationsEnabled, productVendor = ReviewQualityCheckState.ProductVendor.AMAZON, + isSettingsExpanded = isSettingsExpanded, + isInfoExpanded = isInfoExpanded, onReviewGradeLearnMoreClick = {}, onOptOutClick = {}, - onProductRecommendationsEnabledStateChange = {}, + onProductRecommendationsEnabledStateChange = { productRecommendationsEnabled = it }, onFooterLinkClick = {}, - onExpandSettings = {}, + onSettingsExpandToggleClick = { isSettingsExpanded = !isSettingsExpanded }, + onInfoExpandToggleClick = { isInfoExpanded = !isInfoExpanded }, modifier = Modifier.fillMaxWidth(), ) } diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckBottomSheet.kt b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckBottomSheet.kt index 117fcd8ab5..07608506e5 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckBottomSheet.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckBottomSheet.kt @@ -4,7 +4,6 @@ package org.mozilla.fenix.shopping.ui -import androidx.compose.animation.Crossfade import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable @@ -93,14 +92,24 @@ fun ReviewQualityCheckBottomSheet( onRequestDismiss(BottomSheetDismissSource.LINK_OPENED) store.dispatch(ReviewQualityCheckAction.OpenPoweredByLink) }, - onExpandSettings = { - store.dispatch(ReviewQualityCheckAction.ExpandSettingsClicked) + onSettingsExpandToggleClick = { + store.dispatch(ReviewQualityCheckAction.ExpandCollapseSettings) + }, + onInfoExpandToggleClick = { + store.dispatch(ReviewQualityCheckAction.ExpandCollapseInfo) }, onNoAnalysisPresent = { store.dispatch(ReviewQualityCheckAction.NoAnalysisDisplayed) }, - onShowMoreRecentReviewsClicked = { - store.dispatch(ReviewQualityCheckAction.ShowMoreRecentReviewsClicked) + onHighlightsExpandToggleClick = { + store.dispatch(ReviewQualityCheckAction.ExpandCollapseHighlights) + }, + onRecommendedProductClick = { aid, url -> + onRequestDismiss(BottomSheetDismissSource.LINK_OPENED) + store.dispatch(ReviewQualityCheckAction.RecommendedProductClick(aid, url)) + }, + onProductRecommendationImpression = { aid -> + store.dispatch(ReviewQualityCheckAction.RecommendedProductImpression(productAid = aid)) }, ) } @@ -124,67 +133,78 @@ private fun ProductReview( onAnalyzeClick: () -> Unit, onReanalyzeClick: () -> Unit, onProductRecommendationsEnabledStateChange: (Boolean) -> Unit, - onShowMoreRecentReviewsClicked: () -> Unit, + onHighlightsExpandToggleClick: () -> Unit, onNoAnalysisPresent: () -> Unit, - onExpandSettings: () -> Unit, + onSettingsExpandToggleClick: () -> Unit, + onInfoExpandToggleClick: () -> Unit, onReviewGradeLearnMoreClick: () -> Unit, onFooterLinkClick: () -> Unit, + onRecommendedProductClick: (aid: String, url: String) -> Unit, + onProductRecommendationImpression: (aid: String) -> Unit, ) { - Crossfade( - targetState = state.productReviewState, - label = "ProductReview-Crossfade", - ) { productReviewState -> - when (productReviewState) { - is AnalysisPresent -> { - ProductAnalysis( - productRecommendationsEnabled = state.productRecommendationsPreference, - productAnalysis = productReviewState, - productVendor = state.productVendor, - onOptOutClick = onOptOutClick, - onReanalyzeClick = onReanalyzeClick, - onProductRecommendationsEnabledStateChange = onProductRecommendationsEnabledStateChange, - onShowMoreRecentReviewsClicked = onShowMoreRecentReviewsClicked, - onExpandSettings = onExpandSettings, - onReviewGradeLearnMoreClick = onReviewGradeLearnMoreClick, - onFooterLinkClick = onFooterLinkClick, - ) - } - - is ReviewQualityCheckState.OptedIn.ProductReviewState.Error -> { - ProductAnalysisError( - error = productReviewState, - productRecommendationsEnabled = state.productRecommendationsPreference, - productVendor = state.productVendor, - onReviewGradeLearnMoreClick = onReviewGradeLearnMoreClick, - onOptOutClick = onOptOutClick, - onProductRecommendationsEnabledStateChange = onProductRecommendationsEnabledStateChange, - onFooterLinkClick = onFooterLinkClick, - onExpandSettings = onExpandSettings, - modifier = Modifier.fillMaxWidth(), - ) - } + when (val productReviewState = state.productReviewState) { + is AnalysisPresent -> { + ProductAnalysis( + productRecommendationsEnabled = state.productRecommendationsPreference, + productAnalysis = productReviewState, + productVendor = state.productVendor, + isSettingsExpanded = state.isSettingsExpanded, + isInfoExpanded = state.isInfoExpanded, + isHighlightsExpanded = state.isHighlightsExpanded, + onOptOutClick = onOptOutClick, + onReanalyzeClick = onReanalyzeClick, + onProductRecommendationsEnabledStateChange = onProductRecommendationsEnabledStateChange, + onHighlightsExpandToggleClick = onHighlightsExpandToggleClick, + onSettingsExpandToggleClick = onSettingsExpandToggleClick, + onInfoExpandToggleClick = onInfoExpandToggleClick, + onReviewGradeLearnMoreClick = onReviewGradeLearnMoreClick, + onFooterLinkClick = onFooterLinkClick, + onRecommendedProductClick = onRecommendedProductClick, + onRecommendedProductImpression = onProductRecommendationImpression, + ) + } - is ReviewQualityCheckState.OptedIn.ProductReviewState.Loading -> { - ProductReviewLoading() - } + is ReviewQualityCheckState.OptedIn.ProductReviewState.Error -> { + ProductAnalysisError( + error = productReviewState, + productRecommendationsEnabled = state.productRecommendationsPreference, + productVendor = state.productVendor, + isSettingsExpanded = state.isSettingsExpanded, + isInfoExpanded = state.isInfoExpanded, + onReviewGradeLearnMoreClick = onReviewGradeLearnMoreClick, + onOptOutClick = onOptOutClick, + onProductRecommendationsEnabledStateChange = onProductRecommendationsEnabledStateChange, + onFooterLinkClick = onFooterLinkClick, + onSettingsExpandToggleClick = onSettingsExpandToggleClick, + onInfoExpandToggleClick = onInfoExpandToggleClick, + modifier = Modifier.fillMaxWidth(), + ) + } - is ReviewQualityCheckState.OptedIn.ProductReviewState.NoAnalysisPresent -> { - LaunchedEffect(Unit) { - onNoAnalysisPresent() - } + is ReviewQualityCheckState.OptedIn.ProductReviewState.Loading -> { + ProductReviewLoading() + } - NoAnalysis( - isAnalyzing = productReviewState.isReanalyzing, - productRecommendationsEnabled = state.productRecommendationsPreference, - productVendor = state.productVendor, - onAnalyzeClick = onAnalyzeClick, - onReviewGradeLearnMoreClick = onReviewGradeLearnMoreClick, - onOptOutClick = onOptOutClick, - onProductRecommendationsEnabledStateChange = onProductRecommendationsEnabledStateChange, - onExpandSettings = onExpandSettings, - modifier = Modifier.fillMaxWidth(), - ) + is ReviewQualityCheckState.OptedIn.ProductReviewState.NoAnalysisPresent -> { + LaunchedEffect(Unit) { + onNoAnalysisPresent() } + + NoAnalysis( + isAnalyzing = productReviewState.isReanalyzing, + onAnalyzeClick = onAnalyzeClick, + productRecommendationsEnabled = state.productRecommendationsPreference, + productVendor = state.productVendor, + isSettingsExpanded = state.isSettingsExpanded, + isInfoExpanded = state.isInfoExpanded, + onReviewGradeLearnMoreClick = onReviewGradeLearnMoreClick, + onOptOutClick = onOptOutClick, + onProductRecommendationsEnabledStateChange = onProductRecommendationsEnabledStateChange, + onSettingsExpandToggleClick = onSettingsExpandToggleClick, + onInfoExpandToggleClick = onInfoExpandToggleClick, + onFooterLinkClick = onFooterLinkClick, + modifier = Modifier.fillMaxWidth(), + ) } } } diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckCards.kt b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckCards.kt index 796ebde486..7925d96c60 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckCards.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckCards.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import org.mozilla.fenix.R @@ -43,31 +44,35 @@ private val defaultCardContentPadding = 16.dp * A card container for review quality check UI that can be expanded and collapsed. * * @param title The title of the card. + * @param isExpanded Whether or not the card is expanded. * @param modifier Modifier to be applied to the card. - * @param onExpandToggleClick Callback invoked when card is collapsed or expanded. + * @param onExpandToggleClick Invoked when the card is expanded or collapsed. * @param content The content of the card. */ @Composable fun ReviewQualityCheckExpandableCard( title: String, + isExpanded: Boolean, modifier: Modifier = Modifier, - onExpandToggleClick: (isExpanded: Boolean) -> Unit = {}, + onExpandToggleClick: () -> Unit, content: @Composable () -> Unit, ) { ReviewQualityCheckCard( modifier = modifier, contentPadding = PaddingValues(0.dp), ) { - var isExpanded by remember { mutableStateOf(false) } - Row( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier .fillMaxWidth() - .clickable { - isExpanded = isExpanded.not() - onExpandToggleClick(isExpanded) - } + .clickable( + onClickLabel = if (isExpanded) { + stringResource(R.string.a11y_action_label_collapse) + } else { + stringResource(R.string.a11y_action_label_expand) + }, + onClick = onExpandToggleClick, + ) .padding(defaultCardContentPadding), verticalAlignment = Alignment.CenterVertically, ) { @@ -85,7 +90,11 @@ fun ReviewQualityCheckExpandableCard( Icon( painter = painterResource(id = chevronDrawable), - contentDescription = null, + contentDescription = if (isExpanded) { + stringResource(R.string.a11y_state_label_expanded) + } else { + stringResource(R.string.a11y_state_label_collapsed) + }, tint = FirefoxTheme.colors.iconPrimary, ) } @@ -140,6 +149,8 @@ fun ReviewQualityCheckCard( private fun ReviewQualityCheckCardPreview() { FirefoxTheme { Column(modifier = Modifier.padding(16.dp)) { + var isExpanded by remember { mutableStateOf(true) } + ReviewQualityCheckCard( modifier = Modifier.fillMaxWidth(), ) { @@ -155,6 +166,8 @@ private fun ReviewQualityCheckCardPreview() { ReviewQualityCheckExpandableCard( title = "Review Quality Check Expandable Card", modifier = Modifier.fillMaxWidth(), + isExpanded = isExpanded, + onExpandToggleClick = { isExpanded = !isExpanded }, ) { Text( text = "content", diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContextualOnboarding.kt b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContextualOnboarding.kt index ef2698b5af..43cdf5f4f2 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContextualOnboarding.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContextualOnboarding.kt @@ -103,7 +103,7 @@ fun ReviewQualityCheckContextualOnboarding( Text( text = stringResource( - id = R.string.review_quality_check_contextual_onboarding_caption_3, + id = R.string.review_quality_check_contextual_onboarding_caption_2, stringResource(id = R.string.shopping_product_name), ), color = FirefoxTheme.colors.textPrimary, diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckSettingsCard.kt b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckSettingsCard.kt index aaec045ec4..c659188b96 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckSettingsCard.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckSettingsCard.kt @@ -12,6 +12,10 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -26,28 +30,27 @@ import org.mozilla.fenix.theme.FirefoxTheme * the entire review quality check feature. * * @param productRecommendationsEnabled The current state of the product recommendations toggle. + * @param isExpanded Whether or not the settings card is expanded. * @param onProductRecommendationsEnabledStateChange Invoked when the user changes the product * recommendations toggle state. * @param onTurnOffReviewQualityCheckClick Invoked when the user opts out of the review quality check feature. - * @param onExpandSettings Invoked when the user expands the settings card. + * @param onExpandToggleClick Invoked when the user expands or collapses the settings card. * @param modifier Modifier to apply to the layout. */ @Composable fun ReviewQualityCheckSettingsCard( productRecommendationsEnabled: Boolean?, + isExpanded: Boolean, onProductRecommendationsEnabledStateChange: (Boolean) -> Unit, onTurnOffReviewQualityCheckClick: () -> Unit, - onExpandSettings: () -> Unit, + onExpandToggleClick: () -> Unit, modifier: Modifier = Modifier, ) { ReviewQualityCheckExpandableCard( modifier = modifier, title = stringResource(R.string.review_quality_check_settings_title), - onExpandToggleClick = { isExpanded -> - if (isExpanded) { - onExpandSettings() - } - }, + isExpanded = isExpanded, + onExpandToggleClick = onExpandToggleClick, ) { SettingsContent( productRecommendationsEnabled = productRecommendationsEnabled, @@ -95,31 +98,15 @@ private fun ReviewQualityCheckSettingsCardPreview() { .background(color = FirefoxTheme.colors.layer1) .padding(all = 16.dp), ) { - ReviewQualityCheckSettingsCard( - productRecommendationsEnabled = true, - onProductRecommendationsEnabledStateChange = {}, - onTurnOffReviewQualityCheckClick = {}, - onExpandSettings = {}, - modifier = Modifier.fillMaxWidth(), - ) - } - } -} + var isSettingsExpanded by remember { mutableStateOf(true) } + var productRecommendationsEnabled by remember { mutableStateOf(true) } -@LightDarkPreview -@Composable -private fun SettingsContentPreview() { - FirefoxTheme { - Box( - modifier = Modifier - .fillMaxWidth() - .background(color = FirefoxTheme.colors.layer1) - .padding(all = 16.dp), - ) { - SettingsContent( - productRecommendationsEnabled = true, - onProductRecommendationsEnabledStateChange = {}, + ReviewQualityCheckSettingsCard( + productRecommendationsEnabled = productRecommendationsEnabled, + onProductRecommendationsEnabledStateChange = { productRecommendationsEnabled = it }, onTurnOffReviewQualityCheckClick = {}, + isExpanded = isSettingsExpanded, + onExpandToggleClick = { isSettingsExpanded = !isSettingsExpanded }, modifier = Modifier.fillMaxWidth(), ) } diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityInfoCard.kt b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityInfoCard.kt index da91c4b42c..3c703f3271 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityInfoCard.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityInfoCard.kt @@ -15,7 +15,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -35,18 +38,24 @@ import org.mozilla.fenix.theme.FirefoxTheme * Info card UI containing an explanation of the review quality. * * @param productVendor The vendor of the product. + * @param isExpanded Whether or not the card is expanded. * @param modifier Modifier to apply to the layout. + * @param onExpandToggleClick Invoked when the user expands or collapses the card. * @param onLearnMoreClick Invoked when the user clicks to learn more about review grades. */ @Composable fun ReviewQualityInfoCard( productVendor: ReviewQualityCheckState.ProductVendor, + isExpanded: Boolean, modifier: Modifier = Modifier, + onExpandToggleClick: () -> Unit, onLearnMoreClick: () -> Unit, ) { ReviewQualityCheckExpandableCard( title = stringResource(id = R.string.review_quality_check_explanation_title), modifier = modifier, + isExpanded = isExpanded, + onExpandToggleClick = onExpandToggleClick, ) { ReviewQualityInfo( productVendor = productVendor, @@ -189,29 +198,14 @@ private fun ReviewQualityInfoCardPreview() { .background(color = FirefoxTheme.colors.layer1) .padding(all = 16.dp), ) { - ReviewQualityInfoCard( - productVendor = ReviewQualityCheckState.ProductVendor.AMAZON, - modifier = Modifier.fillMaxWidth(), - onLearnMoreClick = {}, - ) - } - } -} + var isInfoExpanded by remember { mutableStateOf(true) } -@Composable -@LightDarkPreview -private fun ReviewQualityInfoPreview() { - FirefoxTheme { - Box( - modifier = Modifier - .fillMaxWidth() - .background(color = FirefoxTheme.colors.layer1) - .padding(all = 16.dp), - ) { - ReviewQualityInfo( + ReviewQualityInfoCard( productVendor = ReviewQualityCheckState.ProductVendor.AMAZON, + isExpanded = isInfoExpanded, modifier = Modifier.fillMaxWidth(), onLearnMoreClick = {}, + onExpandToggleClick = { isInfoExpanded = !isInfoExpanded }, ) } } diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsIntegration.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsIntegration.kt index 1238c71b49..dc39ac1dd2 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsIntegration.kt @@ -15,8 +15,8 @@ import org.mozilla.fenix.ext.components /** * Starts and stops SyncedTabsStorage based on the authentication state. * - * @property context Used to get synced tabs storage, due to cyclic dependency. - * @property accountManager Used to check and observe account authentication state. + * @param context Used to get synced tabs storage, due to cyclic dependency. + * @param accountManager Used to check and observe account authentication state. */ class SyncedTabsIntegration( private val context: Context, diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabSheetBehaviorManager.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabSheetBehaviorManager.kt index 33f3745b40..05acedbb5c 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabSheetBehaviorManager.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabSheetBehaviorManager.kt @@ -30,11 +30,11 @@ private const val DIM_CONVERSION = 1000f /** * Helper class for updating how the tray looks and behaves depending on app state / internal tray state. * - * @property behavior [BottomSheetBehavior] that will actually control the tray. + * @param behavior [BottomSheetBehavior] that will actually control the tray. * @param orientation current Configuration.ORIENTATION_* of the device. - * @property maxNumberOfTabs highest number of tabs in each tray page. - * @property numberForExpandingTray limit depending on which the tray should be collapsed or expanded. - * @property displayMetrics [DisplayMetrics] used for adapting resources to the current display. + * @param maxNumberOfTabs highest number of tabs in each tray page. + * @param numberForExpandingTray limit depending on which the tray should be collapsed or expanded. + * @param displayMetrics [DisplayMetrics] used for adapting resources to the current display. */ internal class TabSheetBehaviorManager( private val behavior: BottomSheetBehavior, diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayBanner.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayBanner.kt index 23bc81ecf4..53450efc76 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayBanner.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayBanner.kt @@ -419,6 +419,7 @@ private fun MultiSelectBanner( IconButton( onClick = onSaveToCollectionsClick, modifier = Modifier.testTag(TabsTrayTestTag.collectionsButton), + enabled = selectedTabCount > 0, ) { Icon( painter = painterResource(id = R.drawable.ic_tab_collection), diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt index d9c9346cc8..1a657533f1 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt @@ -172,26 +172,26 @@ interface TabsTrayController : SyncedTabsController, InactiveTabsController, Tab /** * Default implementation of [TabsTrayController]. * - * @property activity [HomeActivity] used to perform top-level app actions. - * @property appStore [AppStore] used to dispatch any [AppAction]. - * @property tabsTrayStore [TabsTrayStore] used to read/update the [TabsTrayState]. - * @property browserStore [BrowserStore] used to read/update the current [BrowserState]. - * @property settings [Settings] used to update any user preferences. - * @property browsingModeManager [BrowsingModeManager] used to read/update the current [BrowsingMode]. - * @property navController [NavController] used to navigate away from the tabs tray. - * @property navigateToHomeAndDeleteSession Lambda used to return to the Homescreen and delete the current session. - * @property profiler [Profiler] used to add profiler markers. - * @property navigationInteractor [NavigationInteractor] used to perform navigation actions with side effects. - * @property tabsUseCases Use case wrapper for interacting with tabs. - * @property bookmarksUseCase Use case wrapper for interacting with bookmarks. - * @property ioDispatcher [CoroutineContext] used to handle saving tabs as bookmarks. - * @property collectionStorage Storage layer for interacting with collections. - * @property selectTabPosition Lambda used to scroll the tabs tray to the desired position. - * @property dismissTray Lambda used to dismiss/minimize the tabs tray. - * @property showUndoSnackbarForTab Lambda used to display an UNDO Snackbar. + * @param activity [HomeActivity] used to perform top-level app actions. + * @param appStore [AppStore] used to dispatch any [AppAction]. + * @param tabsTrayStore [TabsTrayStore] used to read/update the [TabsTrayState]. + * @param browserStore [BrowserStore] used to read/update the current [BrowserState]. + * @param settings [Settings] used to update any user preferences. + * @param browsingModeManager [BrowsingModeManager] used to read/update the current [BrowsingMode]. + * @param navController [NavController] used to navigate away from the tabs tray. + * @param navigateToHomeAndDeleteSession Lambda used to return to the Homescreen and delete the current session. + * @param profiler [Profiler] used to add profiler markers. + * @param navigationInteractor [NavigationInteractor] used to perform navigation actions with side effects. + * @param tabsUseCases Use case wrapper for interacting with tabs. + * @param bookmarksUseCase Use case wrapper for interacting with bookmarks. + * @param ioDispatcher [CoroutineContext] used to handle saving tabs as bookmarks. + * @param collectionStorage Storage layer for interacting with collections. + * @param selectTabPosition Lambda used to scroll the tabs tray to the desired position. + * @param dismissTray Lambda used to dismiss/minimize the tabs tray. + * @param showUndoSnackbarForTab Lambda used to display an UNDO Snackbar. * @property showCancelledDownloadWarning Lambda used to display a cancelled download warning. - * @property showBookmarkSnackbar Lambda used to display a snackbar upon saving tabs as bookmarks. - * @property showCollectionSnackbar Lambda used to display a snackbar upon successfully saving tabs + * @param showBookmarkSnackbar Lambda used to display a snackbar upon saving tabs as bookmarks. + * @param showCollectionSnackbar Lambda used to display a snackbar upon successfully saving tabs * to a collection. */ @Suppress("TooManyFunctions", "LongParameterList") diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt index 6d19b590c9..b9f8bb5473 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt @@ -110,7 +110,7 @@ interface TabsTrayInteractor : /** * Default implementation of [TabsTrayInteractor]. * - * @property controller [TabsTrayController] to which user actions can be delegated for app updates. + * @param controller [TabsTrayController] to which user actions can be delegated for app updates. */ @Suppress("TooManyFunctions") class DefaultTabsTrayInteractor( diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTabViewHolder.kt index 18ec21eba5..de5a5cd073 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTabViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTabViewHolder.kt @@ -48,11 +48,11 @@ import org.mozilla.fenix.tabstray.ext.toDisplayTitle * A RecyclerView ViewHolder implementation for "tab" items. * * @param itemView [View] that displays a "tab". - * @property imageLoader [ImageLoader] used to load tab thumbnails. - * @property trayStore [TabsTrayStore] containing the complete state of tabs tray and methods to update that. - * @property selectionHolder [SelectionHolder] instance containing the selected tabs in the tabs tray. + * @param imageLoader [ImageLoader] used to load tab thumbnails. + * @param trayStore [TabsTrayStore] containing the complete state of tabs tray and methods to update that. + * @param selectionHolder [SelectionHolder] instance containing the selected tabs in the tabs tray. * @property featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting. - * @property store [BrowserStore] containing the complete state of the browser and methods to update that. + * @param store [BrowserStore] containing the complete state of the browser and methods to update that. */ @Suppress("LongParameterList") abstract class AbstractBrowserTabViewHolder( diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt index b1eb3bfc67..b2286dd27c 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt @@ -29,9 +29,9 @@ import org.mozilla.fenix.tabstray.browser.compose.ComposeListViewHolder /** * A [RecyclerView.Adapter] for browser tabs. * - * @property context [Context] used for various platform interactions or accessing [Components] + * @param context [Context] used for various platform interactions or accessing [Components] * @property interactor [TabsTrayInteractor] handling tabs interactions in a tab tray. - * @property store [TabsTrayStore] containing the complete state of tabs tray and methods to update that. + * @param store [TabsTrayStore] containing the complete state of tabs tray and methods to update that. * @property featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting. * @property viewLifecycleOwner [LifecycleOwner] life cycle owner for the view. */ diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolder.kt index 4d29687fae..e8c96761e3 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolder.kt @@ -30,8 +30,8 @@ import org.mozilla.fenix.GleanMetrics.TabsTray as TabsTrayMetrics * * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. * @param lifecycleOwner [LifecycleOwner] to which this Composable will be tied to. - * @property tabsTrayStore [TabsTrayStore] used to listen for changes to [TabsTrayState.inactiveTabs]. - * @property interactor [InactiveTabsInteractor] used to respond to interactions with the inactive tabs header + * @param tabsTrayStore [TabsTrayStore] used to listen for changes to [TabsTrayState.inactiveTabs]. + * @param interactor [InactiveTabsInteractor] used to respond to interactions with the inactive tabs header * and the auto close dialog. */ @Suppress("LongParameterList") diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAdapter.kt index 5a8044881f..30ce406324 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAdapter.kt @@ -14,9 +14,9 @@ import org.mozilla.fenix.tabstray.TabsTrayStore /** * The adapter for displaying the section of inactive tabs. * - * @property lifecycleOwner [LifecycleOwner] to which the Composable will be tied to. - * @property tabsTrayStore [TabsTrayStore] used to listen for changes to [TabsTrayState.inactiveTabs]. - * @property interactor [InactiveTabsInteractor] used to respond to interactions with the inactive tabs header + * @param lifecycleOwner [LifecycleOwner] to which the Composable will be tied to. + * @param tabsTrayStore [TabsTrayStore] used to listen for changes to [TabsTrayState.inactiveTabs]. + * @param interactor [InactiveTabsInteractor] used to respond to interactions with the inactive tabs header * and the auto close dialog. * @property featureName [String] representing the name of the inactive tabs feature for telemetry reporting. */ diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionBannerBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionBannerBinding.kt index 31dc4139df..34c347966a 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionBannerBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionBannerBinding.kt @@ -28,13 +28,13 @@ import org.mozilla.fenix.tabstray.ext.showWithTheme /** * A binding that shows/hides the multi-select banner of the selected count of tabs. * - * @property context An Android context. - * @property binding The binding used to display the view. - * @property store [TabsTrayStore] used to listen for changes to [TabsTrayState] and dispatch actions. - * @property interactor [TabsTrayInteractor] for responding to user actions. - * @property backgroundView The background view that we want to alter when changing [Mode]. - * @property showOnSelectViews A variable list of views that will be made visible when in select mode. - * @property showOnNormalViews A variable list of views that will be made visible when in normal mode. + * @param context An Android context. + * @param binding The binding used to display the view. + * @param store [TabsTrayStore] used to listen for changes to [TabsTrayState] and dispatch actions. + * @param interactor [TabsTrayInteractor] for responding to user actions. + * @param backgroundView The background view that we want to alter when changing [Mode]. + * @param showOnSelectViews A variable list of views that will be made visible when in select mode. + * @param showOnNormalViews A variable list of views that will be made visible when in normal mode. */ @OptIn(ExperimentalCoroutinesApi::class) @Suppress("LongParameterList") diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionHandleBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionHandleBinding.kt index ee999934a3..a45c38d965 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionHandleBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionHandleBinding.kt @@ -27,8 +27,8 @@ private const val NORMAL_HANDLE_PERCENT_WIDTH = 0.1F * between [Mode]. * * @param store The TabsTrayStore instance. - * @property handle The "handle" of the Tabs Tray that is used to drag the tray open/close. - * @property containerLayout The [ConstraintLayout] that contains the "handle". + * @param handle The "handle" of the Tabs Tray that is used to drag the tray open/close. + * @param containerLayout The [ConstraintLayout] that contains the "handle". */ @OptIn(ExperimentalCoroutinesApi::class) class SelectionHandleBinding( diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTouchHelper.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTouchHelper.kt index 4b9a0bc45f..b9d762f2f9 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTouchHelper.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTouchHelper.kt @@ -51,8 +51,8 @@ class TabsTouchHelper( * An [ItemTouchHelper.Callback] for drawing custom layouts on [RecyclerView.ViewHolder] interactions. * * @param delegate [TabsTray.Delegate] for handling all user interactions. - * @property onViewHolderTouched Invoked when a tab is about to be swiped. See [OnViewHolderTouched]. - * @property onViewHolderDraw Invoked when a tab is drawn. See [OnViewHolderToDraw]. + * @param onViewHolderTouched Invoked when a tab is about to be swiped. See [OnViewHolderTouched]. + * @param onViewHolderDraw Invoked when a tab is drawn. See [OnViewHolderToDraw]. * @param featureNameHolder Contains the identifying name of the feature. * @param onRemove A callback invoked when a tab is removed. */ diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeAbstractTabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeAbstractTabViewHolder.kt index 53fb077396..59f575212f 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeAbstractTabViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeAbstractTabViewHolder.kt @@ -20,8 +20,8 @@ import org.mozilla.fenix.theme.Theme /** * [RecyclerView.ViewHolder] used for Jetpack Compose UI content . * - * @property composeView [ComposeView] which will be populated with Jetpack Compose UI content. - * @property viewLifecycleOwner [LifecycleOwner] life cycle owner for the view. + * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. + * @param viewLifecycleOwner [LifecycleOwner] life cycle owner for the view. */ abstract class ComposeAbstractTabViewHolder( private val composeView: ComposeView, diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeGridViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeGridViewHolder.kt index c2401df505..49d28f5979 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeGridViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeGridViewHolder.kt @@ -24,10 +24,10 @@ import org.mozilla.fenix.tabstray.TabsTrayStore /** * A Compose ViewHolder implementation for "tab" items with grid layout. * - * @property interactor [TabsTrayInteractor] handling tabs interactions in a tab tray. - * @property store [TabsTrayStore] containing the complete state of tabs tray and methods to update that. + * @param interactor [TabsTrayInteractor] handling tabs interactions in a tab tray. + * @param store [TabsTrayStore] containing the complete state of tabs tray and methods to update that. * @param composeItemView that displays a "tab". - * @property featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting. + * @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting. * @param viewLifecycleOwner [LifecycleOwner] to which this Composable will be tied to. */ class ComposeGridViewHolder( diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeListViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeListViewHolder.kt index 4d966c8f0b..acfe9e1b37 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeListViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeListViewHolder.kt @@ -24,10 +24,10 @@ import org.mozilla.fenix.tabstray.TabsTrayStore /** * A Compose ViewHolder implementation for "tab" items with list layout. * - * @property interactor [TabsTrayInteractor] handling tabs interactions in a tab tray. - * @property tabsTrayStore [TabsTrayStore] containing the complete state of tabs tray and methods to update that. + * @param interactor [TabsTrayInteractor] handling tabs interactions in a tab tray. + * @param tabsTrayStore [TabsTrayStore] containing the complete state of tabs tray and methods to update that. * @param composeItemView that displays a "tab". - * @property featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting. + * @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting. * @param viewLifecycleOwner [LifecycleOwner] to which this Composable will be tied to. */ class ComposeListViewHolder( diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ReorderableGrid.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ReorderableGrid.kt index 7c5e8fa340..45a809b1c1 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ReorderableGrid.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ReorderableGrid.kt @@ -77,14 +77,14 @@ fun createGridReorderState( /** * Class containing details about the current state of dragging in grid. * - * @property gridState State of the grid. - * @property scope [CoroutineScope] used for scrolling to the target item. - * @property hapticFeedback [HapticFeedback] used for performing haptic feedback on item long press. - * @property touchSlop Distance in pixels the user can wander until we consider they started dragging. - * @property onMove Callback to be invoked when switching between two items. - * @property onLongPress Optional callback to be invoked when long pressing an item. - * @property onExitLongPress Optional callback to be invoked when the item is dragged after long press. - * @property ignoredItems List of keys for non-draggable items. + * @param gridState State of the grid. + * @param scope [CoroutineScope] used for scrolling to the target item. + * @param hapticFeedback [HapticFeedback] used for performing haptic feedback on item long press. + * @param touchSlop Distance in pixels the user can wander until we consider they started dragging. + * @param onMove Callback to be invoked when switching between two items. + * @param onLongPress Optional callback to be invoked when long pressing an item. + * @param onExitLongPress Optional callback to be invoked when the item is dragged after long press. + * @param ignoredItems List of keys for non-draggable items. */ class GridReorderState internal constructor( private val gridState: LazyGridState, diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ReorderableList.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ReorderableList.kt index cb162d0499..bc0bf49f23 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ReorderableList.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ReorderableList.kt @@ -72,14 +72,14 @@ fun createListReorderState( /** * Class containing details about the current state of dragging in list. * - * @property listState State of the list. - * @property scope [CoroutineScope] used for scrolling to the target item. - * @property hapticFeedback [HapticFeedback] used for performing haptic feedback on item long press. - * @property touchSlop Distance in pixels the user can wander until we consider they started dragging. - * @property onMove Callback to be invoked when switching between two items. - * @property ignoredItems List of keys for non-draggable items. - * @property onLongPress Optional callback to be invoked when long pressing an item. - * @property onExitLongPress Optional callback to be invoked when the item is dragged after long press. + * @param listState State of the list. + * @param scope [CoroutineScope] used for scrolling to the target item. + * @param hapticFeedback [HapticFeedback] used for performing haptic feedback on item long press. + * @param touchSlop Distance in pixels the user can wander until we consider they started dragging. + * @param onMove Callback to be invoked when switching between two items. + * @param ignoredItems List of keys for non-draggable items. + * @param onLongPress Optional callback to be invoked when long pressing an item. + * @param onExitLongPress Optional callback to be invoked when the item is dragged after long press. */ @Suppress("LongParameterList") class ListReorderState internal constructor( diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsIntegration.kt b/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsIntegration.kt index a108176792..2bc7814e53 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsIntegration.kt @@ -24,9 +24,9 @@ import org.mozilla.fenix.tabstray.ext.toSyncedTabsListItem /** * TabsTrayFragment delegate to handle all layout updates needed to display synced tabs and any errors. * - * @property store An instance of [TabsTrayStore] used to manage the tabs tray state. - * @property context Fragment context. - * @property navController The controller used to handle any navigation necessary for error scenarios. + * @param store An instance of [TabsTrayStore] used to manage the tabs tray state. + * @param context Fragment context. + * @param navController The controller used to handle any navigation necessary for error scenarios. * @param storage An instance of [SyncedTabsStorage] used for retrieving synced tabs. * @param accountManager An instance of [FxaAccountManager] used for synced tabs authentication. * @param lifecycleOwner View lifecycle owner used to determine when to cancel UI jobs. diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabsPageViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabsPageViewHolder.kt index d4a60aea5b..58ef98f1f1 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabsPageViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabsPageViewHolder.kt @@ -18,9 +18,9 @@ import org.mozilla.fenix.theme.Theme /** * Temporary ViewHolder to render [SyncedTabsList] until all of the Tabs Tray is written in Compose. * - * @property composeView Root ComposeView passed-in from TrayPagerAdapter. - * @property tabsTrayStore Store used as a Composable State to listen for changes to [TabsTrayState.syncedTabs]. - * @property interactor [SyncedTabsInteractor] used to respond to interactions with synced tabs. + * @param composeView Root ComposeView passed-in from TrayPagerAdapter. + * @param tabsTrayStore Store used as a Composable State to listen for changes to [TabsTrayState.syncedTabs]. + * @param interactor [SyncedTabsInteractor] used to respond to interactions with synced tabs. */ class SyncedTabsPageViewHolder( private val composeView: ComposeView, diff --git a/app/src/main/java/org/mozilla/fenix/telemetry/TelemetryMiddleware.kt b/app/src/main/java/org/mozilla/fenix/telemetry/TelemetryMiddleware.kt index fb7ef13c20..3b448bdb42 100644 --- a/app/src/main/java/org/mozilla/fenix/telemetry/TelemetryMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/telemetry/TelemetryMiddleware.kt @@ -39,13 +39,13 @@ private const val PROGRESS_COMPLETE = 100 /** * [Middleware] to record telemetry in response to [BrowserAction]s. * - * @property context An Android [Context]. - * @property settings reference to the application [Settings]. - * @property metrics [MetricController] to pass events that have been mapped from actions. - * @property crashReporting An instance of [CrashReporting] to report caught exceptions. - * @property nimbusSearchEngine The Nimbus search engine. - * @property searchState Map that stores the [TabSessionState.id] & [TimerId]. - * @property timerId The [TimerId] for the [Metrics.searchPageLoadTime]. + * @param context An Android [Context]. + * @param settings reference to the application [Settings]. + * @param metrics [MetricController] to pass events that have been mapped from actions. + * @param crashReporting An instance of [CrashReporting] to report caught exceptions. + * @param nimbusSearchEngine The Nimbus search engine. + * @param searchState Map that stores the [TabSessionState.id] & [TimerId]. + * @param timerId The [TimerId] for the [Metrics.searchPageLoadTime]. */ class TelemetryMiddleware( private val context: Context, diff --git a/app/src/main/java/org/mozilla/fenix/translations/DownloadIndicator.kt b/app/src/main/java/org/mozilla/fenix/translations/DownloadIndicator.kt new file mode 100644 index 0000000000..952727fe85 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/translations/DownloadIndicator.kt @@ -0,0 +1,157 @@ +/* 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.translations + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.disabled +import androidx.compose.ui.semantics.role +import androidx.compose.ui.unit.dp +import org.mozilla.fenix.R +import org.mozilla.fenix.compose.annotation.LightDarkPreview +import org.mozilla.fenix.compose.button.PrimaryButton +import org.mozilla.fenix.theme.FirefoxTheme + +/** + * Animation duration in milliseconds. + * If it is set to a low number, the speed of the rotation will be higher. + */ +private const val ANIMATION_DURATION_MS = 2000 + +/** + * Icon for Download indicator. + * + * @param icon [Painter] used to be displayed. + * @param modifier [Modifier] to be applied to the icon layout. + * @param tint Tint [Color] to be applied to the icon. + * @param contentDescription Optional content description for the icon. + */ +@Composable +fun DownloadIconIndicator( + icon: Painter, + modifier: Modifier = Modifier, + tint: Color = FirefoxTheme.colors.iconPrimary, + contentDescription: String? = null, +) { + Icon( + painter = icon, + modifier = modifier.then( + Modifier + .rotate(rotationAnimation()), + ), + contentDescription = contentDescription, + tint = tint, + ) +} + +/** + * Download indicator for translations screens. + * It indicates that the download of the language file is in progress. + * + * @param text The button text to be displayed. + * @param modifier [Modifier] to be applied to the layout. + * @param contentDescription Content description to be applied to the button. + * @param icon Optional [Painter] used to display an [Icon] before the button text. + */ +@Composable +fun DownloadIndicator( + text: String, + modifier: Modifier = Modifier, + contentDescription: String? = null, + icon: Painter? = null, +) { + PrimaryButton( + text = text, + modifier = modifier.then( + Modifier + .clearAndSetSemantics { + disabled() + role = Role.Button + contentDescription?.let { this.contentDescription = contentDescription } + }, + ), + enabled = false, + icon = icon, + iconModifier = Modifier + .rotate(rotationAnimation()), + onClick = {}, + ) +} + +@Composable +private fun rotationAnimation(): Float { + val infiniteTransition = rememberInfiniteTransition() + val angle by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 360f, + animationSpec = infiniteRepeatable( + animation = tween(ANIMATION_DURATION_MS, easing = LinearEasing), + repeatMode = RepeatMode.Restart, + ), + ) + return angle +} + +@Composable +@LightDarkPreview +private fun DownloadIconIndicatorPreview() { + FirefoxTheme { + Column( + modifier = Modifier + .background(FirefoxTheme.colors.layer1) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + DownloadIconIndicator( + icon = painterResource(id = R.drawable.mozac_ic_sync_24), + contentDescription = stringResource( + id = R.string.translations_bottom_sheet_translating_in_progress, + ), + ) + } + } +} + +@Composable +@LightDarkPreview +private fun DownloadIndicatorPreview() { + FirefoxTheme { + Column( + modifier = Modifier + .background(FirefoxTheme.colors.layer1) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + DownloadIndicator( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + text = stringResource(id = R.string.translations_bottom_sheet_translating_in_progress), + contentDescription = stringResource( + id = R.string.translations_bottom_sheet_translating_in_progress_content_description, + ), + icon = painterResource(id = R.drawable.mozac_ic_sync_24), + ) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/translations/TranslationOptionsDialog.kt b/app/src/main/java/org/mozilla/fenix/translations/TranslationOptionsDialog.kt new file mode 100644 index 0000000000..71588dd1ba --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/translations/TranslationOptionsDialog.kt @@ -0,0 +1,190 @@ +/* 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.translations + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import org.mozilla.fenix.R +import org.mozilla.fenix.compose.Divider +import org.mozilla.fenix.compose.SwitchWithLabel +import org.mozilla.fenix.compose.annotation.LightDarkPreview +import org.mozilla.fenix.compose.list.TextListItem +import org.mozilla.fenix.theme.FirefoxTheme +import java.util.Locale + +/** + * Firefox Translation options bottom sheet dialog. + * + * @param translationOptionsList A list of [TranslationSwitchItem]s to display. + * @param onBackClicked Invoked when the user clicks on the back button. + * @param onTranslationSettingsClicked Invoked when the user clicks on the "Translation Settings" button. + * @param aboutTranslationClicked Invoked when the user clicks on the "About Translation" button. + */ +@Composable +fun TranslationOptionsDialog( + translationOptionsList: List, + onBackClicked: () -> Unit, + onTranslationSettingsClicked: () -> Unit, + aboutTranslationClicked: () -> Unit, +) { + TranslationOptionsDialogHeader(onBackClicked) + + LazyColumn { + items(translationOptionsList) { item: TranslationSwitchItem -> + TranslationOptions(translationSwitchItem = item) + } + + item { + TextListItem( + label = stringResource(id = R.string.translation_option_bottom_sheet_translation_settings), + modifier = Modifier + .fillMaxWidth() + .padding(start = 56.dp), + onClick = { onTranslationSettingsClicked() }, + ) + } + + item { + TextListItem( + label = stringResource( + id = R.string.translation_option_bottom_sheet_about_translations, + formatArgs = arrayOf(stringResource(R.string.app_name)), + ), + modifier = Modifier + .fillMaxWidth() + .padding(start = 56.dp), + onClick = { aboutTranslationClicked() }, + ) + } + } +} + +@Composable +private fun TranslationOptions(translationSwitchItem: TranslationSwitchItem) { + SwitchWithLabel( + checked = translationSwitchItem.isChecked, + onCheckedChange = translationSwitchItem.onStateChange, + label = translationSwitchItem.textLabel, + modifier = Modifier + .padding(start = 72.dp, end = 16.dp), + ) + + if (translationSwitchItem.hasDivider) { + Divider(Modifier.padding(top = 4.dp, bottom = 4.dp)) + } +} + +@Composable +private fun TranslationOptionsDialogHeader( + onBackClicked: () -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(end = 16.dp, start = 16.dp) + .defaultMinSize(minHeight = 56.dp), + ) { + IconButton( + onClick = { onBackClicked() }, + modifier = Modifier + .size(24.dp), + ) { + Icon( + painter = painterResource(id = R.drawable.mozac_ic_back_24), + contentDescription = stringResource(R.string.etp_back_button_content_description), + tint = FirefoxTheme.colors.iconPrimary, + ) + } + + Spacer(modifier = Modifier.width(32.dp)) + + Text( + text = stringResource(id = R.string.translation_option_bottom_sheet_title), + modifier = Modifier + .weight(1f) + .semantics { heading() }, + color = FirefoxTheme.colors.textPrimary, + style = FirefoxTheme.typography.headline7, + ) + } +} + +/** + * Return a list of Translation option switch list item. + */ +@Composable +fun getTranslationOptionsList(): List { + return mutableListOf().apply { + add( + TranslationSwitchItem( + textLabel = stringResource(R.string.translation_option_bottom_sheet_always_translate), + isChecked = false, + hasDivider = true, + onStateChange = {}, + ), + ) + add( + TranslationSwitchItem( + textLabel = stringResource( + id = R.string.translation_option_bottom_sheet_always_translate_in_language, + formatArgs = arrayOf(Locale("es").displayName), + ), + isChecked = false, + hasDivider = false, + onStateChange = {}, + ), + ) + add( + TranslationSwitchItem( + textLabel = stringResource( + id = R.string.translation_option_bottom_sheet_never_translate_in_language, + formatArgs = arrayOf(Locale("es").displayName), + ), + isChecked = true, + hasDivider = true, + onStateChange = {}, + ), + ) + add( + TranslationSwitchItem( + textLabel = stringResource(R.string.translation_option_bottom_sheet_never_translate_site), + isChecked = true, + hasDivider = true, + onStateChange = {}, + ), + ) + } +} + +@Composable +@LightDarkPreview +private fun TranslationSettingsPreview() { + FirefoxTheme { + TranslationOptionsDialog( + translationOptionsList = getTranslationOptionsList(), + onBackClicked = {}, + onTranslationSettingsClicked = {}, + aboutTranslationClicked = {}, + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/translations/TranslationSettings.kt b/app/src/main/java/org/mozilla/fenix/translations/TranslationSettings.kt new file mode 100644 index 0000000000..1d37fdeacf --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/translations/TranslationSettings.kt @@ -0,0 +1,150 @@ +/* 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.translations + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import org.mozilla.fenix.R +import org.mozilla.fenix.compose.Divider +import org.mozilla.fenix.compose.SwitchWithLabel +import org.mozilla.fenix.compose.annotation.LightDarkPreview +import org.mozilla.fenix.compose.list.TextListItem +import org.mozilla.fenix.theme.FirefoxTheme + +/** + * Firefox Translation settings fragment compose view. + * + * @param translationSwitchList list of [TranslationSwitchItem]s to display. + * @param onAutomaticTranslationClicked Invoked when the user clicks on the "Automatic Translation" button. + * @param onNeverTranslationClicked Invoked when the user clicks on the "Never Translation" button. + * @param onDownloadLanguageClicked Invoked when the user clicks on the "Download Language" button. + */ +@Composable +fun TranslationSettings( + translationSwitchList: List, + onAutomaticTranslationClicked: () -> Unit, + onNeverTranslationClicked: () -> Unit, + onDownloadLanguageClicked: () -> Unit, +) { + Column( + modifier = Modifier + .background( + color = FirefoxTheme.colors.layer1, + ), + ) { + LazyColumn { + items(translationSwitchList) { item: TranslationSwitchItem -> + SwitchWithLabel( + checked = item.isChecked, + onCheckedChange = item.onStateChange, + label = item.textLabel, + modifier = Modifier + .padding(start = 72.dp, end = 16.dp), + ) + + if (item.hasDivider) { + Divider(Modifier.padding(top = 8.dp, bottom = 8.dp)) + } + } + + item { + Text( + text = stringResource( + id = R.string.translation_settings_translation_preference, + ), + modifier = Modifier + .fillMaxWidth() + .padding(start = 72.dp, end = 16.dp, bottom = 16.dp, top = 8.dp) + .semantics { heading() }, + color = FirefoxTheme.colors.textAccent, + style = FirefoxTheme.typography.headline8, + ) + } + + item { + TextListItem( + label = stringResource(id = R.string.translation_settings_automatic_translation), + modifier = Modifier + .fillMaxWidth() + .padding(start = 56.dp), + onClick = { onAutomaticTranslationClicked() }, + ) + } + + item { + TextListItem( + label = stringResource( + id = R.string.translation_settings_automatic_never_translate_sites, + ), + modifier = Modifier + .fillMaxWidth() + .padding(start = 56.dp), + onClick = { onNeverTranslationClicked() }, + ) + } + + item { + TextListItem( + label = stringResource( + id = R.string.translation_settings_download_language, + ), + modifier = Modifier + .fillMaxWidth() + .padding(start = 56.dp), + onClick = { onDownloadLanguageClicked() }, + ) + } + } + } +} + +/** + * Return a list of Translation option switch list item. + */ +@Composable +internal fun getTranslationSettingsSwitchList(): List { + return mutableListOf().apply { + add( + TranslationSwitchItem( + textLabel = stringResource(R.string.translation_settings_offer_to_translate), + isChecked = true, + hasDivider = false, + onStateChange = {}, + ), + ) + add( + TranslationSwitchItem( + textLabel = stringResource(R.string.translation_settings_always_download), + isChecked = false, + hasDivider = true, + onStateChange = {}, + ), + ) + } +} + +@Composable +@LightDarkPreview +private fun TranslationSettingsPreview() { + FirefoxTheme { + TranslationSettings( + translationSwitchList = getTranslationSettingsSwitchList(), + onAutomaticTranslationClicked = {}, + onDownloadLanguageClicked = {}, + onNeverTranslationClicked = {}, + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/translations/TranslationSettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/translations/TranslationSettingsFragment.kt new file mode 100644 index 0000000000..43088b5774 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/translations/TranslationSettingsFragment.kt @@ -0,0 +1,53 @@ +/* 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.translations + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import mozilla.components.support.base.feature.UserInteractionHandler +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.showToolbar +import org.mozilla.fenix.theme.FirefoxTheme + +/** + * A fragment displaying the Firefox Translation settings screen. + */ +class TranslationSettingsFragment : Fragment(), UserInteractionHandler { + override fun onResume() { + super.onResume() + showToolbar(getString(R.string.translation_settings_toolbar_title)) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View = ComposeView(requireContext()).apply { + setContent { + FirefoxTheme { + TranslationSettings( + translationSwitchList = getTranslationSettingsSwitchList(), + onAutomaticTranslationClicked = {}, + onDownloadLanguageClicked = {}, + onNeverTranslationClicked = {}, + ) + } + } + } + + override fun onBackPressed(): Boolean { + findNavController().navigate( + TranslationSettingsFragmentDirections.actionTranslationSettingsFragmentToTranslationsDialogFragment( + TranslationsDialogAccessPoint.TranslationsOptions, + ), + ) + return true + } +} diff --git a/app/src/main/java/org/mozilla/fenix/translations/TranslationSwitchItem.kt b/app/src/main/java/org/mozilla/fenix/translations/TranslationSwitchItem.kt new file mode 100644 index 0000000000..58efbab329 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/translations/TranslationSwitchItem.kt @@ -0,0 +1,21 @@ +/* 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.translations + +/** + * TranslationSwitchItem that will appear on Translation screens. + * + * @property textLabel The text that will appear on the switch item. + * @property isChecked Whether the switch is checked or not. + * @property hasDivider Whether a divider should appear under the switch item. + * @property onStateChange Invoked when the switch item is clicked, + * the new checked state is passed into the callback. + */ +data class TranslationSwitchItem( + val textLabel: String, + val isChecked: Boolean, + val hasDivider: Boolean, + val onStateChange: (Boolean) -> Unit, +) diff --git a/app/src/main/java/org/mozilla/fenix/translations/TranslationsBottomSheet.kt b/app/src/main/java/org/mozilla/fenix/translations/TranslationsBottomSheet.kt new file mode 100644 index 0000000000..fe48a95704 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/translations/TranslationsBottomSheet.kt @@ -0,0 +1,146 @@ +/* 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.translations + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandIn +import androidx.compose.animation.fadeIn +import androidx.compose.animation.slideInHorizontally +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Divider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import org.mozilla.fenix.theme.FirefoxTheme + +private const val BOTTOM_SHEET_HANDLE_WIDTH_PERCENT = 0.1f + +@Composable +internal fun TranslationDialogBottomSheet(content: @Composable () -> Unit) { + Column( + modifier = Modifier + .background( + color = FirefoxTheme.colors.layer2, + shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp), + ) + .nestedScroll(rememberNestedScrollInteropConnection()), + ) { + Divider( + Modifier + .padding(top = 16.dp) + .fillMaxWidth(BOTTOM_SHEET_HANDLE_WIDTH_PERCENT) + .align(alignment = Alignment.CenterHorizontally), + color = FirefoxTheme.colors.borderInverted, + thickness = 3.dp, + ) + + content() + } +} + +@Composable +internal fun TranslationsAnimation( + translationsVisibility: Boolean, + density: Density, + translationsOptionsHeightDp: Dp, + content: @Composable AnimatedVisibilityScope.() -> Unit, +) { + AnimatedVisibility( + visible = translationsVisibility, + enter = expandIn( + animationSpec = tween( + easing = FastOutSlowInEasing, + ), + initialSize = { + with(density) { + IntSize( + 0, + translationsOptionsHeightDp.roundToPx(), + ) + } + }, + ) + fadeIn( + animationSpec = tween( + easing = FastOutSlowInEasing, + ), + ) + slideInHorizontally( + animationSpec = tween( + easing = FastOutSlowInEasing, + ), + ), + ) { + content() + } +} + +@Composable +internal fun TranslationsOptionsAnimation( + translationsVisibility: Boolean, + density: Density, + translationsHeightDp: Dp, + translationsWidthDp: Dp, + content: @Composable AnimatedVisibilityScope.() -> Unit, +) { + AnimatedVisibility( + visible = translationsVisibility, + enter = expandIn( + animationSpec = tween( + easing = FastOutSlowInEasing, + ), + initialSize = { + with(density) { + IntSize( + 0, + translationsHeightDp.roundToPx(), + ) + } + }, + ) + fadeIn( + animationSpec = tween( + easing = FastOutSlowInEasing, + ), + ) + slideInHorizontally( + initialOffsetX = { with(density) { translationsWidthDp.roundToPx() } }, + animationSpec = tween( + easing = FastOutSlowInEasing, + ), + ), + ) { + content() + } +} + +@Composable +internal fun TranslationsDialog(onSettingClicked: () -> Unit) { + TranslationsDialogBottomSheet( + onSettingClicked = onSettingClicked, + ) +} + +@Composable +internal fun TranslationsOptionsDialog( + onBackClicked: () -> Unit, + onTranslationSettingsClicked: () -> Unit, +) { + TranslationOptionsDialog( + translationOptionsList = getTranslationOptionsList(), + onBackClicked = onBackClicked, + onTranslationSettingsClicked = onTranslationSettingsClicked, + aboutTranslationClicked = {}, + ) +} diff --git a/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBottomSheet.kt b/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBottomSheet.kt index cd19ba78f9..2a8c767909 100644 --- a/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBottomSheet.kt +++ b/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBottomSheet.kt @@ -4,7 +4,6 @@ package org.mozilla.fenix.translations -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -14,7 +13,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Divider import androidx.compose.material.Icon import androidx.compose.material.IconButton @@ -25,6 +23,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import org.mozilla.fenix.R import org.mozilla.fenix.compose.annotation.LightDarkPreview @@ -36,16 +36,12 @@ import org.mozilla.fenix.theme.FirefoxTheme * Firefox Translations bottom sheet dialog. */ @Composable -fun TranslationsDialogBottomSheet() { +fun TranslationsDialogBottomSheet(onSettingClicked: () -> Unit) { Column( modifier = Modifier - .background( - color = FirefoxTheme.colors.layer2, - shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp), - ) .padding(16.dp), ) { - TranslationsDialogHeader() + TranslationsDialogHeader(onSettingClicked) Spacer(modifier = Modifier.height(14.dp)) @@ -68,13 +64,15 @@ fun TranslationsDialogBottomSheet() { } @Composable -private fun TranslationsDialogHeader() { +private fun TranslationsDialogHeader(onSettingClicked: () -> Unit) { Row( verticalAlignment = Alignment.CenterVertically, ) { Text( text = stringResource(id = R.string.translations_bottom_sheet_title), - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .semantics { heading() }, color = FirefoxTheme.colors.textPrimary, style = FirefoxTheme.typography.headline7, ) @@ -82,12 +80,12 @@ private fun TranslationsDialogHeader() { Spacer(modifier = Modifier.width(4.dp)) IconButton( - onClick = {}, + onClick = { onSettingClicked() }, modifier = Modifier.size(24.dp), ) { Icon( painter = painterResource(id = R.drawable.mozac_ic_settings_24), - contentDescription = null, + contentDescription = stringResource(id = R.string.translation_option_bottom_sheet_title), tint = FirefoxTheme.colors.iconPrimary, ) } @@ -152,6 +150,6 @@ private fun TranslationsDialogActionButtons() { @LightDarkPreview private fun TranslationsDialogBottomSheetPreview() { FirefoxTheme { - TranslationsDialogBottomSheet() + TranslationsDialogBottomSheet(onSettingClicked = {}) } } diff --git a/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogFragment.kt index 0df688450b..f1774ef937 100644 --- a/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogFragment.kt @@ -9,24 +9,42 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment import org.mozilla.fenix.R import org.mozilla.fenix.theme.FirefoxTheme +/** + * The enum is to know what bottom sheet to open. + */ +enum class TranslationsDialogAccessPoint { + Translations, TranslationsOptions, +} + /** * A bottom sheet fragment displaying the Firefox Translation dialog. */ class TranslationsDialogFragment : BottomSheetDialogFragment() { private var behavior: BottomSheetBehavior? = null + private val args by navArgs() override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = super.onCreateDialog(savedInstanceState).apply { setOnShowListener { - val bottomSheet = - findViewById(R.id.design_bottom_sheet) + val bottomSheet = findViewById(R.id.design_bottom_sheet) bottomSheet?.setBackgroundResource(android.R.color.transparent) behavior = BottomSheetBehavior.from(bottomSheet) } @@ -39,7 +57,79 @@ class TranslationsDialogFragment : BottomSheetDialogFragment() { ): View = ComposeView(requireContext()).apply { setContent { FirefoxTheme { - TranslationsDialogBottomSheet() + var translationsVisibility by remember { + mutableStateOf(args.translationsDialogAccessPoint == TranslationsDialogAccessPoint.Translations) + } + + var translationsHeightDp by remember { + mutableStateOf(0.dp) + } + + var translationsOptionsHeightDp by remember { + mutableStateOf(0.dp) + } + + var translationsWidthDp by remember { + mutableStateOf(0.dp) + } + + val density = LocalDensity.current + + TranslationDialogBottomSheet { + TranslationsAnimation( + translationsVisibility = translationsVisibility, + density = density, + translationsOptionsHeightDp = translationsOptionsHeightDp, + ) { + if (translationsVisibility) { + Column( + modifier = Modifier.onGloballyPositioned { coordinates -> + translationsHeightDp = with(density) { + coordinates.size.height.toDp() + } + translationsWidthDp = with(density) { + coordinates.size.width.toDp() + } + }, + ) { + TranslationsDialog( + onSettingClicked = { + translationsVisibility = false + }, + ) + } + } + } + + TranslationsOptionsAnimation( + translationsVisibility = !translationsVisibility, + density = density, + translationsHeightDp = translationsHeightDp, + translationsWidthDp = translationsWidthDp, + ) { + if (!translationsVisibility) { + Column( + modifier = Modifier.onGloballyPositioned { coordinates -> + translationsOptionsHeightDp = with(density) { + coordinates.size.height.toDp() + } + }, + ) { + TranslationsOptionsDialog( + onBackClicked = { + translationsVisibility = true + }, + onTranslationSettingsClicked = { + findNavController().navigate( + TranslationsDialogFragmentDirections + .actionTranslationsDialogFragmentToTranslationSettingsFragment(), + ) + }, + ) + } + } + } + } } } } diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index 1777d09f25..21f934d74b 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -46,6 +46,11 @@ import org.mozilla.fenix.nimbus.CookieBannersSection import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.nimbus.HomeScreenSection import org.mozilla.fenix.nimbus.Mr2022Section +import org.mozilla.fenix.nimbus.QueryParameterStrippingSection +import org.mozilla.fenix.nimbus.QueryParameterStrippingSection.QUERY_PARAMETER_STRIPPING +import org.mozilla.fenix.nimbus.QueryParameterStrippingSection.QUERY_PARAMETER_STRIPPING_ALLOW_LIST +import org.mozilla.fenix.nimbus.QueryParameterStrippingSection.QUERY_PARAMETER_STRIPPING_PMB +import org.mozilla.fenix.nimbus.QueryParameterStrippingSection.QUERY_PARAMETER_STRIPPING_STRIP_LIST import org.mozilla.fenix.settings.PhoneFeature import org.mozilla.fenix.settings.deletebrowsingdata.DeleteBrowsingDataOnQuitType import org.mozilla.fenix.settings.logins.SavedLoginsSortingStrategyMenu @@ -62,7 +67,7 @@ private const val AUTOPLAY_USER_SETTING = "AUTOPLAY_USER_SETTING" /** * A simple wrapper for SharedPreferences that makes reading preference a little bit easier. * - * @property appContext Reference to application context. + * @param appContext Reference to application context. */ @Suppress("LargeClass", "TooManyFunctions") class Settings(private val appContext: Context) : PreferencesHolder { @@ -611,61 +616,41 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = true, ) - var shouldUseCookieBanner by lazyFeatureFlagPreference( - appContext.getPreferenceKey(R.string.pref_key_cookie_banner_v1), - featureFlag = true, - default = { cookieBannersSection[CookieBannersSection.FEATURE_SETTING_VALUE] == 1 }, - ) - var shouldUseCookieBannerPrivateMode by lazyFeatureFlagPreference( appContext.getPreferenceKey(R.string.pref_key_cookie_banner_private_mode), featureFlag = true, - default = { cookieBannersSection[CookieBannersSection.FEATURE_SETTING_VALUE_PBM] == 1 }, - ) - - var userOptOutOfReEngageCookieBannerDialog by booleanPreference( - appContext.getPreferenceKey(R.string.pref_key_cookie_banner_re_engage_dialog_dismissed), - default = false, + default = { shouldUseCookieBannerPrivateModeDefaultValue }, ) - var lastInteractionWithReEngageCookieBannerDialogInMs by longPreference( - appContext.getPreferenceKey( - R.string.pref_key_cookie_banner_re_engage_dialog_last_interaction_in_ms, - ), - default = 0L, - ) + val shouldUseCookieBannerPrivateModeDefaultValue: Boolean + get() = cookieBannersSection[CookieBannersSection.FEATURE_SETTING_VALUE_PBM] == 1 - var cookieBannerDetectedPreviously by booleanPreference( - appContext.getPreferenceKey(R.string.pref_key_cookie_banner_first_banner_detected), - default = false, - ) + val shouldUseCookieBanner: Boolean + get() = cookieBannersSection[CookieBannersSection.FEATURE_SETTING_VALUE] == 1 val shouldShowCookieBannerUI: Boolean get() = cookieBannersSection[CookieBannersSection.FEATURE_UI] == 1 - /** - * Indicates after how many hours a cookie banner dialog should be shown again - */ - @VisibleForTesting - internal val timerForCookieBannerDialog: Long - get() = 60 * 60 * 1000L * - (cookieBannersSection[CookieBannersSection.DIALOG_RE_ENGAGE_TIME] ?: 4) + val shouldEnableCookieBannerDetectOnly: Boolean + get() = cookieBannersSection[CookieBannersSection.FEATURE_SETTING_DETECT_ONLY] == 1 - /** - * Indicates if we should should show the cookie banner dialog that invites the user to turn-on - * the setting. - */ - fun shouldShowCookieBannerReEngagementDialog(): Boolean { - val shouldShowDialog = - shouldShowCookieBannerUI && cookieBannerReEngagementDialogShowsCount.underMaxCount() && - !userOptOutOfReEngageCookieBannerDialog && !shouldUseCookieBanner - return if (shouldShowDialog) { - !cookieBannerDetectedPreviously || - timeNowInMillis() - lastInteractionWithReEngageCookieBannerDialogInMs >= timerForCookieBannerDialog - } else { - false - } - } + val shouldEnableCookieBannerGlobalRules: Boolean + get() = cookieBannersSection[CookieBannersSection.FEATURE_SETTING_GLOBAL_RULES] == 1 + + val shouldEnableCookieBannerGlobalRulesSubFrame: Boolean + get() = cookieBannersSection[CookieBannersSection.FEATURE_SETTING_GLOBAL_RULES_SUB_FRAMES] == 1 + + val shouldEnableQueryParameterStripping: Boolean + get() = queryParameterStrippingSection[QUERY_PARAMETER_STRIPPING] == "1" + + val shouldEnableQueryParameterStrippingPrivateBrowsing: Boolean + get() = queryParameterStrippingSection[QUERY_PARAMETER_STRIPPING_PMB] == "1" + + val queryParameterStrippingAllowList: String + get() = queryParameterStrippingSection[QUERY_PARAMETER_STRIPPING_ALLOW_LIST].orEmpty() + + val queryParameterStrippingStripList: String + get() = queryParameterStrippingSection[QUERY_PARAMETER_STRIPPING_STRIP_LIST].orEmpty() /** * Declared as a function for performance purposes. This could be declared as a variable using @@ -796,6 +781,15 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = { feltPrivateBrowsingEnabled }, ) + /** + * Indicates if the cookie banners CRF should be shown. + */ + var shouldShowCookieBannersCFR by lazyFeatureFlagPreference( + appContext.getPreferenceKey(R.string.pref_key_should_show_cookie_banners_action_popup), + featureFlag = true, + default = { shouldShowCookieBannerUI }, + ) + val blockCookiesSelectionInCustomTrackingProtection by stringPreference( key = appContext.getPreferenceKey(R.string.pref_key_tracking_protection_custom_cookies_select), default = if (enabledTotalCookieProtection) { @@ -1530,6 +1524,10 @@ class Settings(private val appContext: Context) : PreferencesHolder { get() = FxNimbus.features.cookieBanners.value().sectionsEnabled + private val queryParameterStrippingSection: Map + get() = + FxNimbus.features.queryParameterStripping.value().sectionsEnabled + private val homescreenSections: Map get() = FxNimbus.features.homescreen.value().sectionsEnabled @@ -1803,9 +1801,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { fun getCookieBannerHandling(): CookieBannerHandlingMode { return when (shouldUseCookieBanner) { true -> CookieBannerHandlingMode.REJECT_ALL - false -> if (shouldShowCookieBannerReEngagementDialog()) { - CookieBannerHandlingMode.REJECT_ALL - } else { + false -> { CookieBannerHandlingMode.DISABLED } } @@ -1823,14 +1819,6 @@ class Settings(private val appContext: Context) : PreferencesHolder { } } - /** - * Times that the cookie banner re-engagement dialog has been shown. - */ - val cookieBannerReEngagementDialogShowsCount = counterPreference( - appContext.getPreferenceKey(R.string.pref_key_cookie_banner_re_engagement_dialog_shows_counter), - maxCount = 2, - ) - var setAsDefaultGrowthSent by booleanPreference( key = appContext.getPreferenceKey(R.string.pref_key_growth_set_as_default), default = false, diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/LegacyWallpaperMigration.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/LegacyWallpaperMigration.kt index e3ab62e792..5bc8926a35 100644 --- a/app/src/main/java/org/mozilla/fenix/wallpapers/LegacyWallpaperMigration.kt +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/LegacyWallpaperMigration.kt @@ -19,9 +19,9 @@ import java.io.IOException /** * Manages the migration of legacy wallpapers to the new paths * - * @property storageRootDirectory The top level app-local storage directory. - * @property settings Used to update the color of the text shown above wallpapers. - * @property downloadWallpaper Function used to download assets for legacy drawable wallpapers. + * @param storageRootDirectory The top level app-local storage directory. + * @param settings Used to update the color of the text shown above wallpapers. + * @param downloadWallpaper Function used to download assets for legacy drawable wallpapers. */ class LegacyWallpaperMigration( private val storageRootDirectory: File, diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperDownloader.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperDownloader.kt index eac034584f..2beff12a7b 100644 --- a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperDownloader.kt +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperDownloader.kt @@ -18,9 +18,9 @@ import java.lang.IllegalStateException /** * Can download wallpapers from a remote host. * - * @property storageRootDirectory The top level app-local storage directory. - * @property client Required for fetching files from network. - * @property dispatcher Dispatcher used to execute suspending functions. Default parameter + * @param storageRootDirectory The top level app-local storage directory. + * @param client Required for fetching files from network. + * @param dispatcher Dispatcher used to execute suspending functions. Default parameter * should be likely be used except for when under test. */ class WallpaperDownloader( diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperFileManager.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperFileManager.kt index 2c9112d259..e31ac7662c 100644 --- a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperFileManager.kt +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperFileManager.kt @@ -16,8 +16,8 @@ import java.io.File /** * Manages various functions related to the locally-stored wallpaper assets. * - * @property storageRootDirectory The top level app-local storage directory. - * @property coroutineDispatcher Dispatcher used to execute suspending functions. Default parameter + * @param storageRootDirectory The top level app-local storage directory. + * @param coroutineDispatcher Dispatcher used to execute suspending functions. Default parameter * should be likely be used except for when under test. */ class WallpaperFileManager( diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperMetadataFetcher.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperMetadataFetcher.kt index d16d894729..96a50ee57e 100644 --- a/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperMetadataFetcher.kt +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/WallpaperMetadataFetcher.kt @@ -19,7 +19,7 @@ import java.util.Locale /** * Utility class for downloading wallpaper metadata from the remote server. * - * @property client The client that will be used to fetch metadata. + * @param client The client that will be used to fetch metadata. */ class WallpaperMetadataFetcher( private val client: Client, diff --git a/app/src/main/res/color/toggle_text_color.xml b/app/src/main/res/color/toggle_text_color.xml deleted file mode 100644 index 9ca6b7f284..0000000000 --- a/app/src/main/res/color/toggle_text_color.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_open_in_regular_tab.xml b/app/src/main/res/drawable/ic_open_in_regular_tab.xml new file mode 100644 index 0000000000..79c44fdcf6 --- /dev/null +++ b/app/src/main/res/drawable/ic_open_in_regular_tab.xml @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_private_mode_circle_fill_48.xml b/app/src/main/res/drawable/ic_private_mode_circle_fill_48.xml deleted file mode 100644 index bf23a4541c..0000000000 --- a/app/src/main/res/drawable/ic_private_mode_circle_fill_48.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/search_pill_background.xml b/app/src/main/res/drawable/search_pill_background.xml deleted file mode 100644 index e5b157d91c..0000000000 --- a/app/src/main/res/drawable/search_pill_background.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/search_pill_background_unselected.xml b/app/src/main/res/drawable/search_pill_background_unselected.xml deleted file mode 100644 index b66f68bd3f..0000000000 --- a/app/src/main/res/drawable/search_pill_background_unselected.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/search_pill_drawable_button_background.xml b/app/src/main/res/drawable/search_pill_drawable_button_background.xml deleted file mode 100644 index 37f2eb4868..0000000000 --- a/app/src/main/res/drawable/search_pill_drawable_button_background.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/component_cookie_banner_details_panel.xml b/app/src/main/res/layout/component_cookie_banner_details_panel.xml index 6c8a734f8c..486bb64b6e 100644 --- a/app/src/main/res/layout/component_cookie_banner_details_panel.xml +++ b/app/src/main/res/layout/component_cookie_banner_details_panel.xml @@ -31,17 +31,29 @@ android:paddingTop="16dp" android:textColor="?attr/textPrimary" android:textSize="16sp" - app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/navigate_back" + app:layout_constraintEnd_toStartOf="@id/cookieBannerSwitch" app:layout_constraintTop_toTopOf="parent" - tools:text="Turn off Cookize Banner Reduction for [domain.com]? " /> + tools:text="Turn off Cookie Banner Blocker for [domain.com]?" /> + - diff --git a/app/src/main/res/layout/fragment_pwa_onboarding.xml b/app/src/main/res/layout/fragment_pwa_onboarding.xml index cbe2268a4b..58b0eb911b 100644 --- a/app/src/main/res/layout/fragment_pwa_onboarding.xml +++ b/app/src/main/res/layout/fragment_pwa_onboarding.xml @@ -47,7 +47,7 @@ app:layout_constraintEnd_toStartOf="@id/description" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/dialog_title" - app:srcCompat="@drawable/mozac_ic_add_to_home_screen_24" /> + app:srcCompat="@drawable/mozac_ic_add_to_homescreen_24" /> + app:constraint_referenced_ids="awesome_bar,keyboard_divider"/> @@ -149,44 +149,9 @@ app:layout_constraintTop_toBottomOf="@id/fill_link_from_clipboard" /> - - - - - - + app:layout_constraintBottom_toBottomOf="parent" /> diff --git a/app/src/main/res/layout/full_screen_notification_dialog.xml b/app/src/main/res/layout/full_screen_notification_dialog.xml new file mode 100644 index 0000000000..6bc78ae7ea --- /dev/null +++ b/app/src/main/res/layout/full_screen_notification_dialog.xml @@ -0,0 +1,40 @@ + + + + + + + + diff --git a/app/src/main/res/layout/preference_switch_learn_more.xml b/app/src/main/res/layout/preference_switch_learn_more.xml deleted file mode 100644 index c43c3d1e17..0000000000 --- a/app/src/main/res/layout/preference_switch_learn_more.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 8df79b6a6f..736dc350c6 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -164,7 +164,6 @@ - @@ -299,15 +298,12 @@ - + app:destination="@id/translations_graph" /> - - - - - @@ -1442,4 +1420,29 @@ app:popUpToInclusive="true" /> + + + + + + + + + + diff --git a/app/src/main/res/raw/initial_experiments.json b/app/src/main/res/raw/initial_experiments.json index 258fc7e567..33f3f456d3 100644 --- a/app/src/main/res/raw/initial_experiments.json +++ b/app/src/main/res/raw/initial_experiments.json @@ -47,7 +47,7 @@ ], "targeting": "((is_already_enrolled) || ((isFirstRun == 'true') && (app_version|versionCompare('116.!') >= 0)))", "startDate": "2023-08-09", - "enrollmentEndDate": "2023-08-22", + "enrollmentEndDate": null, "endDate": null, "proposedDuration": 28, "proposedEnrollment": 7, @@ -58,139 +58,34 @@ }, { "schemaVersion": "1.12.0", - "slug": "android-onboarding-search-widget", - "id": "android-onboarding-search-widget", + "slug": "android-re-engagement-notifications-timing-v2-copy", + "id": "android-re-engagement-notifications-timing-v2-copy", "arguments": {}, "application": "org.mozilla.firefox", "appName": "fenix", "appId": "org.mozilla.firefox", "channel": "release", - "userFacingName": "Android Onboarding - search widget", - "userFacingDescription": "Copy focused on privacy", + "userFacingName": "Android re-engagement private browser notifications v3", + "userFacingDescription": "Notification to bring awareness to the private browsing option. (excludes the DE audience so that they can see a different notification)", "isEnrollmentPaused": false, "isRollout": false, "bucketConfig": { "randomizationUnit": "nimbus_id", - "namespace": "fenix-juno-onboarding-release-5", + "namespace": "fenix-re-engagement-notification-release-5", "start": 0, "count": 10000, "total": 10000 }, "featureIds": [ - "juno-onboarding" + "re-engagement-notification" ], "probeSets": [], "outcomes": [ { "slug": "default-browser", - "priority": "secondary" - } - ], - "branches": [ - { - "slug": "control", - "ratio": 1, - "feature": { - "featureId": "this-is-included-for-mobile-pre-96-support", - "enabled": false, - "value": {} - }, - "features": [ - { - "featureId": "juno-onboarding", - "enabled": true, - "value": { - "enabled": true - } - } - ] - }, - { - "slug": "treatment-a", - "ratio": 1, - "feature": { - "featureId": "this-is-included-for-mobile-pre-96-support", - "enabled": false, - "value": {} - }, - "features": [ - { - "featureId": "juno-onboarding", - "enabled": true, - "value": { - "enabled": true, - "cards": { - "sync-sign-in": { - "enabled": false - }, - "add-search-widget": { - "enabled": true - } - } - } - } - ] - }, - { - "slug": "treatment-b", - "ratio": 1, - "feature": { - "featureId": "this-is-included-for-mobile-pre-96-support", - "enabled": false, - "value": {} - }, - "features": [ - { - "featureId": "juno-onboarding", - "enabled": true, - "value": { - "enabled": true, - "cards": { - "add-search-widget": { - "enabled": true - } - } - } - } - ] + "priority": "primary" } ], - "targeting": "((android_sdk_version|versionCompare('26') >= 0)) && (app_version|versionCompare('118.!') >= 0) && (language in ['en'])", - "startDate": "2023-09-15", - "enrollmentEndDate": "2023-10-24", - "endDate": null, - "proposedDuration": 49, - "proposedEnrollment": 28, - "referenceBranch": "control", - "featureValidationOptOut": false, - "localizations": null, - "locales": null - }, - { - "schemaVersion": "1.12.0", - "slug": "android-re-engagement-notifications-timing-v2", - "id": "android-re-engagement-notifications-timing-v2", - "arguments": {}, - "application": "org.mozilla.firefox", - "appName": "fenix", - "appId": "org.mozilla.firefox", - "channel": "release", - "userFacingName": "Android re-engagement notifications timing v2", - "userFacingDescription": "Testing timing of how we enable re-engagement notifications.", - "isEnrollmentPaused": true, - "isRollout": false, - "bucketConfig": { - "randomizationUnit": "nimbus_id", - "namespace": "fenix-re-engagement-notification-release-3", - "start": 5000, - "count": 5000, - "total": 10000 - }, - "featureIds": [ - "re-engagement-notification" - ], - "probeSets": [], - "outcomes": [], "branches": [ { "slug": "control-off-branch", @@ -230,9 +125,9 @@ ] } ], - "targeting": "((is_already_enrolled) || ((isFirstRun == 'true') && (app_version|versionCompare('115.!') >= 0)))", - "startDate": "2023-06-28", - "enrollmentEndDate": "2023-07-20", + "targeting": "((is_already_enrolled) || ((isFirstRun == 'true') && (app_version|versionCompare('120.!') >= 0) && (region in ['AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AO', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BQ', 'BR', 'BS', 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CW', 'CX', 'CY', 'CZ', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER', 'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE', 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE', 'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'SS', 'ST', 'SV', 'SX', 'SY', 'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'YE', 'YT', 'ZA', 'ZM', 'ZW'])))", + "startDate": "2023-11-16", + "enrollmentEndDate": null, "endDate": null, "proposedDuration": 30, "proposedEnrollment": 14, @@ -243,37 +138,37 @@ }, { "schemaVersion": "1.12.0", - "slug": "beta-android-onboarding-redesign-treatment-a-rollout", - "id": "beta-android-onboarding-redesign-treatment-a-rollout", + "slug": "splash-screen-test", + "id": "splash-screen-test", "arguments": {}, - "application": "org.mozilla.firefox_beta", + "application": "org.mozilla.firefox", "appName": "fenix", - "appId": "org.mozilla.firefox_beta", - "channel": "beta", - "userFacingName": "Beta Android Onboarding Redesign - Treatment A Rollout", - "userFacingDescription": "Testing a new onboarding experience.", + "appId": "org.mozilla.firefox", + "channel": "release", + "userFacingName": "Splash screen Test", + "userFacingDescription": "Testing a splasshcreen on app launch.", "isEnrollmentPaused": false, - "isRollout": true, + "isRollout": false, "bucketConfig": { "randomizationUnit": "nimbus_id", - "namespace": "fenix-juno-onboarding-beta-mobile_first_run-rollout-1", + "namespace": "fenix-splash-screen-release-2", "start": 0, "count": 10000, "total": 10000 }, "featureIds": [ - "juno-onboarding" + "splash-screen" ], "probeSets": [], "outcomes": [ { - "slug": "default-browser", + "slug": "onboarding", "priority": "primary" } ], "branches": [ { - "slug": "treatment-a", + "slug": "control", "ratio": 1, "feature": { "featureId": "this-is-included-for-mobile-pre-96-support", @@ -282,54 +177,17 @@ }, "features": [ { - "featureId": "juno-onboarding", + "featureId": "splash-screen", "enabled": true, "value": { - "enabled": true + "enabled": false, + "maximum_duration_ms": 0 } } ] - } - ], - "targeting": "((is_already_enrolled) || ((isFirstRun == 'true') && (app_version|versionCompare('118.!') >= 0)))", - "startDate": "2023-09-06", - "enrollmentEndDate": "2023-09-11", - "endDate": null, - "proposedDuration": 28, - "proposedEnrollment": 7, - "referenceBranch": "treatment-a", - "featureValidationOptOut": false, - "localizations": null, - "locales": null - }, - { - "schemaVersion": "1.12.0", - "slug": "fx-release-android-re-engagement-notifications-114-rollout-v2", - "id": "fx-release-android-re-engagement-notifications-114-rollout-v2", - "arguments": {}, - "application": "org.mozilla.firefox", - "appName": "fenix", - "appId": "org.mozilla.firefox", - "channel": "release", - "userFacingName": "Fx Release - Android re-engagement notifications 116 rollout v2", - "userFacingDescription": "Android message for Fx 116", - "isEnrollmentPaused": false, - "isRollout": true, - "bucketConfig": { - "randomizationUnit": "nimbus_id", - "namespace": "fenix-re-engagement-notification-release-mobile_first_run-rollout-2", - "start": 0, - "count": 10000, - "total": 10000 - }, - "featureIds": [ - "re-engagement-notification" - ], - "probeSets": [], - "outcomes": [], - "branches": [ + }, { - "slug": "control", + "slug": "treatment-b", "ratio": 1, "feature": { "featureId": "this-is-included-for-mobile-pre-96-support", @@ -338,87 +196,26 @@ }, "features": [ { - "featureId": "re-engagement-notification", + "featureId": "splash-screen", "enabled": true, "value": { "enabled": true, - "type": 0 + "maximum_duration_ms": 6000 } } ] } ], - "targeting": "(app_version|versionCompare('116.*') <= 0) && ((is_already_enrolled) || ((isFirstRun == 'true') && (app_version|versionCompare('116.!') >= 0)))", - "startDate": "2023-07-21", - "enrollmentEndDate": "2023-09-19", + "targeting": "((is_already_enrolled) || ((isFirstRun == 'true') && (app_version|versionCompare('121.!') >= 0) && (region in ['AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AO', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BQ', 'BR', 'BS', 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CW', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER', 'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE', 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE', 'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'SS', 'ST', 'SV', 'SX', 'SY', 'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'YE', 'YT', 'ZA', 'ZM', 'ZW'])))", + "startDate": "2023-12-06", + "enrollmentEndDate": null, "endDate": null, - "proposedDuration": 60, - "proposedEnrollment": 60, + "proposedDuration": 49, + "proposedEnrollment": 21, "referenceBranch": "control", "featureValidationOptOut": false, "localizations": null, "locales": null - }, - { - "schemaVersion": "1.12.0", - "slug": "release-android-onboarding-redesign-treatment-a-rollout", - "id": "release-android-onboarding-redesign-treatment-a-rollout", - "arguments": {}, - "application": "org.mozilla.firefox", - "appName": "fenix", - "appId": "org.mozilla.firefox", - "channel": "release", - "userFacingName": "[release] Android Onboarding Redesign - Treatment A Rollout", - "userFacingDescription": "Testing a new onboarding experience.", - "isEnrollmentPaused": false, - "isRollout": true, - "bucketConfig": { - "randomizationUnit": "nimbus_id", - "namespace": "fenix-juno-onboarding-release-mobile_first_run-rollout-1", - "start": 0, - "count": 10000, - "total": 10000 - }, - "featureIds": [ - "juno-onboarding" - ], - "probeSets": [], - "outcomes": [ - { - "slug": "default-browser", - "priority": "primary" - } - ], - "branches": [ - { - "slug": "treatment-a", - "ratio": 1, - "feature": { - "featureId": "this-is-included-for-mobile-pre-96-support", - "enabled": false, - "value": {} - }, - "features": [ - { - "featureId": "juno-onboarding", - "enabled": true, - "value": { - "enabled": true - } - } - ] - } - ], - "targeting": "((is_already_enrolled) || ((isFirstRun == 'true') && (app_version|versionCompare('116.!') >= 0)))", - "startDate": "2023-07-12", - "enrollmentEndDate": "2023-08-08", - "endDate": null, - "proposedDuration": 28, - "proposedEnrollment": 7, - "referenceBranch": "treatment-a", - "featureValidationOptOut": false, - "localizations": null, - "locales": null } ] } diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 75bc1aa289..fd248b0ff1 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -522,6 +522,11 @@ Дазволіць + + %1$s толькі што адмовіўся ад кукі для вас + + Менш адцягнення ўвагі, менш кукі, якія асочваюць вас на гэтым сайце. + Аўтаматычна спрабуе падключацца да сайтаў з выкарыстаннем пратаколу шыфравання HTTPS для павышэння бяспекі. @@ -631,6 +636,8 @@ Дадаткі + + Усталяваць дадатак з файла Абвесткі @@ -2271,8 +2278,6 @@ Рэклама ад %s - Сродак праверкі водгукаў працуе на %s. - Сродак праверкі водгукаў працуе на %s %s ад Mozilla @@ -2332,6 +2337,8 @@ Паспрабуйце наш надзейны даведнік па водгуках на прадукты Паглядзіце, наколькі надзейныя водгукі аб прадукце на %1$s, перш чым купляць. Праверка водгукаў, эксперыментальная функцыя ад %2$s, убудавана прама ў браўзер. Таксама працуе на %3$s і %4$s. + + Паглядзіце, наколькі надзейныя водгукі аб прадукце на %1$s, перш чым купляць. Праверка водгукаў, эксперыментальная функцыя ад %2$s, убудавана прама ў браўзер. Выкарыстоўваючы магчымасці %1$s ад Mozilla, мы дапамагаем вам пазбегнуць неаб’ектыўных і несапраўдных водгукаў. Наша мадэль штучнага інтэлекту пастаянна ўдасканальваецца, каб абараніць вас у часе куплі. %2$s @@ -2398,4 +2405,6 @@ прачытаць артыкул адкрыйце спасылку, каб даведацца больш + + %s, загаловак diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml index 2ca304aa32..fdc252012e 100644 --- a/app/src/main/res/values-br/strings.xml +++ b/app/src/main/res/values-br/strings.xml @@ -64,6 +64,14 @@ Mojennoù a vez alies diwar-benn ar merdeiñ prevez + + + Na lezel roud ebet war an trevnad-mañ + + Piv a c’hallfe gwelet ar pezh ar ran? + Lañsit hoc’h ivinell prevez a zeu en ur stokadenn. @@ -182,6 +190,8 @@ Levraoueg Lecʼhienn urzhiataer + + Digeriñ en un ivinell ordinal Ouzhpennañ dʼar skramm degemer @@ -222,6 +232,9 @@ Skramm degemer + + Skarzhañ ar roll istor Yezh dibabet @@ -234,7 +247,7 @@ Cʼhwilerviñ - Lusker enklask + Lusker enklask Arventennoù al lusker klask @@ -297,19 +310,14 @@ Ket bremañ - - Lakait %s da vezañ ho merdeer dre ziouer - Lakait Firefox da vezañ ho merdeer dre ziouer - - %1$s a lak an dud a-raok an arc’hant ha difenn a ra ho puhez prevez en ur stankañ an heulierien etre al lec’hiennoù. \n\nDeskit hiroc’h en hor %2$s. + Lakait Firefox da vezañ ho merdeer dre ziouer + + Ho surentez a zo talvoudus deomp - Firefox a lak an dud a-raok an arc’hant ha difenn a ra ho puhez prevez en ur stankañ an heulierien etre al lec’hiennoù. \n\nDeskit hiroc’h en hor evezhiadennoù a-fet buhez prevez. + Firefox a lak an dud a-raok an arc’hant ha difenn a ra ho puhez prevez en ur stankañ an heulierien etre al lec’hiennoù. \n\nDeskit hiroc’h en hor evezhiadennoù a-fet buhez prevez. evezhiadennoù a-fet buhez prevez @@ -318,30 +326,30 @@ Ket bremañ - Tremenit deus ar pellgomz d’an urzhiataer hezoug hag ar c’hontrol + Tremenit deus ar pellgomz d’an urzhiataer hezoug hag ar c’hontrol - Adtapit ivinelloù ha gerioù-tremen diouzh ho trevnadoù all evit distreiñ e-lec’h ma oac’h. + Adtapit ivinelloù ha gerioù-tremen diouzh ho trevnadoù all evit distreiñ e-lec’h ma oac’h. Kennaskañ Ket bremañ - - Gant ar rebuzadurioù e c’hallit ober muioc’h a draoù gant %s - Gant ar rebuzadurioù e c’hallit ober muioc’h a draoù gant Firefox - - Kasit ivinelloù etre ho trevnadoù, merit pellgargadurioù, lennit alioù evit tapout seizh gwellañ gwarez buhez prevez %s. + Gant ar rebuzadurioù e c’hallit ober muioc’h a draoù gant Firefox + + Gant ar rebuzadurioù e c’hallit chom suroc\'h gant Firefox - Kasit ivinelloù etre ho trevnadoù, merit pellgargadurioù, lennit alioù evit tapout seizh gwellañ gwarez buhez prevez Firefox. + Note: The word "Firefox" should NOT be translated --> + Kasit ivinelloù etre ho trevnadoù, merit pellgargadurioù, lennit alioù evit tapout seizh gwellañ gwarez buhez prevez Firefox. Gweredekaat ar rebuzadurioù Ket bremañ + + Ket bremañ + Digeriñ en un ivinell %1$s nevez @@ -362,7 +370,7 @@ Dibabit unan - Merañ ar berradennoù klask + Merañ ar berradennoù klask Embann al luskerioù a c’haller gwelet el lañser klask @@ -371,8 +379,14 @@ Keflusker enklask dre ziouer Klask + + Luskerioù klask - Barrenn chomlecʼhioù + Barrenn chomlecʼhioù + + Barrenn chomlec\'hioù - Alioù Firefox + + Gouzout hiroc\'h diwar-benn ar pezh a vez aliet gant Firefox Notennit war Google Play - Bihanadur banniel an toupinoù + Bihanadur banniel an toupinoù - Bihanaat banniel an toupinoù + Bihanaat banniel an toupinoù - Diweredekaet + Diweredekaet - Gweredekaet + Gweredekaet - %1$s a glask nac’hañ ar goulennoù toupinoù ent emgefreek pa vez skrammet ur banniel toupinoù. + %1$s a glask nac’hañ ar goulennoù toupinoù ent emgefreek pa vez skrammet ur banniel toupinoù. Diweredekaet evit al lec’hienn-mañ @@ -427,26 +441,26 @@ N’eo ket skoret al lec’hienn-mañ c’hoazh - Gweredekaat bihanaat banniel an toupinoù evit %1$s? + Gweredekaat bihanaat banniel an toupinoù evit %1$s? - Diweredekaat bihanaat banniel an toupinoù evit %1$s? + Diweredekaat bihanaat banniel an toupinoù evit %1$s? %1$s n’hall ket nac’hañ ar goulennoù toupinoù war al lec’hienn-mañ. Gallout a rit goulenn e vefe skoret al lec’hienn-mañ en dazont. - %1$s a skarzho toupinoù al lec’hienn-mañ hag adkargañ a raio ar bajenn. Skarzhañ an holl doupinoù a c’hallfe digennaskañ ac’hanoc’h pe goullonderiñ ho panerioù. + %1$s a skarzho toupinoù al lec’hienn-mañ hag adkargañ a raio ar bajenn. Skarzhañ an holl doupinoù a c’hallfe digennaskañ ac’hanoc’h pe goullonderiñ ho panerioù. - %1$s a c’hall klask nac’hañ ent emgefreek ar goulennoù toupinoù war al lec’hiennoù skoret. + %1$s a c’hall klask nac’hañ ent emgefreek ar goulennoù toupinoù war al lec’hiennoù skoret. - Aotren %1$s da nac’hañ ar goulennoù toupinoù? + Aotren %1$s da nac’hañ ar goulennoù toupinoù? - %1$s a c’hall nac’hañ ent emgefreek goulennoù ar bannieloù toupinoù. + %1$s a c’hall nac’hañ ent emgefreek goulennoù ar bannieloù toupinoù. - Ket bremañ + Ket bremañ - Nebeutoc’h a c’houlennoù toupinoù a vo gwelet ganeoc’h + Nebeutoc’h a c’houlennoù toupinoù a vo gwelet ganeoc’h - Aotren + Aotren Klask kennaskañ ent emgefreek gant ar c’homenad HTTPS evit muioc’h a surentez. @@ -471,11 +485,13 @@ Haezadusted - Dafariad kont Firefox personelaet + Dafariad kont Firefox personelaet + + Dafariad kont Mozilla personelaet Dafariad Sync personelaet - Dafariad kont Firefox/Sync kemmet. Kuitaet e vo an arload evit arloañ ar cʼhemmoù… + Dafariad kont Firefox/Sync kemmet. Kuitaet e vo an arload evit arloañ ar cʼhemmoù… Kont @@ -491,7 +507,9 @@ Kennaskit evit goubredañ an ivinelloù, sinedoù, gerioù-tremen ha muioc’h c’hoazh. - Kont Firefox + Kont Firefox + + Kont Mozilla Adkennaskit evit kendercʼhel gant ar goubredañ @@ -503,7 +521,7 @@ Diveugañ a-bell dre USB - Diskouez al luskerioù klask + Diskouez al luskerioù klask Diskouez kinnigoù ar cʼhlask @@ -522,6 +540,11 @@ Arventennoù ar gont Leuniañ an ereoù ent emgefreek + + Kinnigoù paeroniet + + Alioù %1$s Digeriñ ereoù en arloadoù @@ -604,17 +627,10 @@ %s klasel - - Embannadur bevennet - Steudadoù arzour·ez - - Dastumad nevez ar Mouezhioù Dizalc’h. %s Dastumad ar Mouezhioù Dizalc’h. %s - - Dastumad nevez ar Mouezhioù Dizalc’h. Dastumad ar Mouezhioù Dizalc’h. @@ -624,12 +640,24 @@ Dizoleiñ skeudennoù drekleur all + + + Askouezhioù nevez zo da gaout + - Ne vez skoret an enlugellad-mañ + Ne vez skoret an enlugellad-mañ - An enlugellad-mañ a zo bet staliet endeo + An enlugellad-mañ a zo bet staliet endeo + + + + Diweredekaet eo an askouezhioù evit ar mare + + Klask adloc’hañ an askouezhioù + + Kenderc’hel gant an askouezhioù bet diweredekaet @@ -927,10 +955,10 @@ Digeriñ an ivinelloù Anv an dastumad - - Adenvel - - Dilemel + + Adenvel + + Dilemel Dilemel eus ar roll istor @@ -1298,6 +1326,8 @@ Ivinell prevez serret Ivinelloù prevez serret + + Dilamet eo bet ar roadennoù merdeiñ prevez DIZOBER @@ -1346,12 +1376,11 @@ %d ivinell - Roll istor merdeiñ ha roadennoù lec’hiennoù + + Roll istor merdeiñ %d chomlec’h - - Toupinoù Digennasket e viot eus darn al lec’hiennoù @@ -1406,63 +1435,10 @@ Strollad dilemet - - Donemat war un internet gwelloc’h - - Ur merdeer savet evit an dud ha n’eo ket ar gounidoù. - - Adloc’hit el lec’h m’ho peus paouezet - - Goubredit an ivinelloù hag ar gerioù-tremen a-dreuz ho trevnadoù evit tremen eus ur skramm d’egile digudenn. - - Kennaskañ Enaouet eo Sync - - Gwarez ar vuhez prevez dre ziouer - - %1$s a harz en un doare emgefreek ouzh an embregerezhioù d’ho heuliañ dre guzh war ar web. - - Gant ar gwarez a-enep an toupinoù e c’hallit harzañ heulierien da implij an toupinoù evit spiañ ac’hanoc’h a-dreuz al lec’hiennoù. - - Skoueriek (dre ziouer) - - Kempouezet evit prevezded hag efedusted. Ar pajennoù a garg en un doare ordinal. - - Strizh - - Stankañ muioc’h a heulierien hag ar pajennoù da gargañ buanoc’h, met arc’hweladurioù zo a c’hallo mont a-dreuz. - - Dibabit e pelec’h lakaat ho parrenn ostilhoù - - Mirit anezhi en traoñ pe lakait anezhi en nec’h. - - Mestroniañ a rit ho roadennoù - - Firefox a ro deoc’h ar galloud war ar pezh a c’hallit rannañ enlinenn hag ar pezh a rannit ganeomp. - - Lennit hon evezhiadenn a-get buhez prevez - - - Prest d’ober anaoudegezh gant un internet digredus? - - Kregiñ da verdeiñ - - Dibabit an neuz - - - Espernit batiri hag ho sell gant ar mod teñval. - - Emgefreek - - Azas a raio da arventennoù ho trevnad - - Neuz teñval - - Neuz sklaer - Ivinelloù kaset! @@ -1874,26 +1850,26 @@ Embann al lusker klask - Ouzhpennañ + Ouzhpennañ - Enrollañ + Enrollañ Embann Dilemel - All + All Anv - Anv + Anv Anv ar c’heflusker enklask URL ar chadenn glask - Testenn glask da implij + Testenn glask da implij URL da implij evit ar c’hlask @@ -1905,8 +1881,6 @@ API alioù klask (diret) URL API an alioù klask - - Erlec’hiañ ar c’hlask gant “%s”. Skouer:\nhttp://suggestqueries.google.com/complete/search?client=firefox&q=%s Enrollañ @@ -2036,14 +2010,14 @@ Berradennoù - - Anv + + Anv Anv ar verradenn - - Mat eo - - Nullañ + + Mat eo + + Nullañ Arventennoù @@ -2121,15 +2095,129 @@ Mont d’an arventennoù + + + Gwirier alioù + + Gwirier alioù + + Alioù fizius + + Meskaj alioù fizius ha disfizius + + Alioù disfizius + + Pegen fizius eo an alioù-se? + + Alioù disfizius dilamet + + Penaos e termenomp perzhded an alioù + + D’hor soñj e c’haller kaout fiziañs en alioù-se. + + Ni a soñj deomp ez eo fizius an alioù-se. + + Gouzout muioc’h diwar-benn %s. + + penaos e termen %s gant Mozilla perzhded an alioù + + penaos e vez priziet perzhded an alioù gant %s + + Arventennoù + + Diskouez bruderezh er gwirier alioù + + Gouzout hiroc’h + + Diweredekaat ar gwirier alioù + + Produioù all + + Bruderezh gant %s + + %s gant Mozilla + + Gwiriañ bremañ + + N\'eus ket a-walc’h a alioù c\'hoazh + + Dihegerz eo ar produ-mañ + + O wiriañ perzhded an ali + + O wiriañ perzhded an ali + + Gallout a ra padout tro-dro 60 eilenn. + + Trugarez da vezañ danevellet! + + N’hallomp ket gwiriañ an alioù-mañ + + Titouroù all a vo bremaik + + Komprenet am eus + + Titour ebet da gaout evit ar mare + + Kennask ebet ouzh ar rouedad + + Titour ebet diwar-benn an alioù-se c’hoazh + + Gwiriañ perzhded an alioù + + Gouzout hiroc’h + + politikerezh a-fet buhez prevez + + Politikerezh a-fet buhez prevez + + termenoù implij + + Termenoù implij + + Ya, esaeañ anezhañ + + Ket bremañ + + Esaeit ar gwirier alioù + + Digeriñ ar gwirier alioù + + Beta + + Digeriñ ar gwirier alioù + + Serriñ ar gwirier alioù + + %1$s steredenn war 5 + + Diskouez nebeutoc’h + + Diskouez muioc’h + + Perzhded + + Priz + + Kas + + Pakadur ha neuz + + Kevezerezh + bihanaat + + kuzhet astenn + + dispaket digeriñ an ere evit gouzout hiroc’h diwar-benn an dastumad-mañ lenn ar pennad digeriñ an ere da c’houzout hiroc’h - + diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index e27e8964e8..08fc543179 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -355,6 +355,31 @@ Note: The word "Firefox" should NOT be translated --> Obavještenja vam pomažu da ostanete sigurniji sa Firefoxom + + Šaljite tabove između uređaja, upravljajte preuzimanjima i dobijajte savjete o tome kako iskoristiti Firefox na najbolji način. + + Sigurno šaljite tabove između svojih uređaja i otkrijte druge funkcije privatnosti u Firefoxu. + + Uključite obavještenja + + Ne sada + + Isprobajte Firefox widget za pretraživanje + + Sa Firefoxom na početnom ekranu, imat ćete lahak pristup pretraživaču koji je na prvom mjestu za privatnost i koji blokira praćenje na različitim lokacijama. + + Dodaj Firefox widget + + Ne sada + + + + Otvori novi %1$s tab Traži @@ -369,12 +394,32 @@ Opće O Fenixu + + Izaberi jedno + + Upravljajte prečicama za pretraživanje + + Upravljajte alternativnim pretraživačima + + Uredi pretraživače vidljive u meniju za pretragu + + Pretraživači vidljivi u meniju za pretragu Glavni pretraživač Pretraga + + Pretraživači + + Prijedlozi od pretraživača Adresna traka + + Postavke adresne trake + + Adresna traka - Firefox prijedlozi + + Saznajte više o Firefox prijedlozima Ocijenite na Google Play @@ -395,27 +440,128 @@ Otvaraj linkove u privatnom tabu Dozvoli screenshote u privatnom surfanju + + Ako je dozvoljeno, privatni tabovi će također biti vidljivi kada je otvoreno više aplikacija Dodaj prečicu za privatno surfanje + + Način rada samo za HTTPS + + + Smanjivanje pojavljivanja dijaloga kolačića + + Blokiranje pojavljivanja dijaloga kolačića + + Smanjivanje pojavljivanja dijaloga kolačića u privatnom pretraživanju + + Smanji pojavljivanje dijaloga kolačića + + Isključeno + + Uključeno + + %1$s automatski pokušava odbiti zahtjeve za kolačiće na dijalozima kolačića. + + Isključeno za ovu stranicu + + Otkaži + + Pošalji zahtjev + + Tražiti podršku za ovu stranicu? + + Zahtjev poslan + + Uključeno za ovu stranicu + + Zahtjev za podršku je poslan + + Stranica trenutno nije podržana + + Uključiti smanjenje dijaloga kolačića za %1$s? + + Uključiti blokiranje dijaloga kolačića za %1$s? + + Isključiti smanjenje dijaloga kolačića za %1$s? + + Isključiti blokiranje dijaloga kolačića za %1$s? + + + %1$s ne može automatski odbiti zahtjeve za kolačiće na ovoj stranici. Možete poslati zahtjev za podršku ovoj stranici u budućnosti. + + %1$s će očistiti kolačiće ove stranice i osvježiti stranicu. Čišćenje svih kolačića može vas odjaviti ili isprazniti kolica za kupovinu. + + Isključite i %1$s će očistiti kolačiće i ponovo učitati ovu stranicu. Ovo vas može odjaviti ili isprazniti korpe za kupovinu. + + %1$s pokušava automatski odbiti sve zahtjeve za kolačiće na podržanim web stranicama. + + Uključite i %1$s će pokušati automatski odbiti sve banere kolačića na ovoj stranici. + + Dozvoliti %1$s da odbije banere kolačića? + + %1$s može automatski odbiti mnoge zahtjeve za kolačićima. + + Ne sada + + Vidjet ćete manje zahtjeva za kolačiće + + Dozvoli + + %1$s je upravo odbio kolačiće umjesto vas + + Manje ometanja, manje kolačića koji vas prate na ovoj stranici. + + + Automatski pokušava da se poveže na stranice koristeći HTTPS protokol za šifrovanje radi povećane sigurnosti. + + Isključeno + + Uključeno na svim tabovima + + Uključeno u privatnim tabovima + + Saznajte više + + Omogući u svim tabovima + + Omogući samo u privatnim tabovima + + Sigurna stranica nije dostupna + + Najvjerovatnije web stranica jednostavno ne podržava HTTPS. + + Međutim, takođe je moguće da je umiješan i napadač. Ako nastavite na web stranicu, ne biste trebali unositi nikakve osjetljive informacije. Ako nastavite, način rada samo za HTTPS će biti privremeno isključen za web stranicu. Pristupačnost Zaseban Firefox Account server + + Prilagođeni server Mozilla računa Zaseban Sync server Firefox Account/Sync server promijenjen. Ugasite aplikaciju za primjenu promjena… + + Mozilla račun/Sync server izmijenjen. Napuštanje aplikacije radi primjene promjena… Račun Alatna traka Tema + + Početna stranica + + Gestovi Prilagodi + + Prijavite se da sinhronizujete tabove, oznake, lozinke i još mnogo toga. Firefox račun + + Mozilla račun Ponovo se povežite za nastavak sinhronizacije @@ -440,19 +586,144 @@ Pretraži historiju surfanja Pretraži zabilješke + + Pretraži sinhronizovane tabove Postavke računa + + Automatsko dovršavanje URL-ova + + Prijedlozi sponzora + + Podržite %1$s povremenim sponzorisanim prijedlozima + + Prijedlozi od %1$s + + Dobijte prijedloge s weba koji se odnose na vašu pretragu Otvori linkove u aplikacijama + + Uvijek + + Pitaj prije otvaranja + + Nikad Vanjski menadžer preuzimanja + + Omogući Gecko dnevnike + + Napuštanje aplikacije radi primjene promjena… + Add-oni + + Instaliraj dodatak iz datoteke Obavijesti + + Dozvoljeno + + Nije dozvoljeno + + + + Kolekcija prilagođenih dodataka + + OK + + Otkaži + + Naziv kolekcije + + Vlasnik kolekcije (korisnički ID) + + Izmijenjena kolekcija dodataka. Napuštanje aplikacije radi primjene promjena… + + + + Vrati se nazad + + Nedavne oznake + + Nedavno posjećeno + + Priče koje podstiču na razmišljanje + + Članke omogućava %s + + Sponzorisane priče + + Pozadine + + Sponzorisane prečice + + + + Stavka pozadine: %1$s + + Pozadina ažurirana! + + Prikaži + + Preuzimanje pozadine nije uspjelo + + Pokušaj ponovo + + Nije moguće promijeniti pozadinu + + Saznajte više + + Klasični %s + + Umetnička serija + + Kolekcija nezavisnih glasova. %s + + Kolekcija nezavisnih glasova. + + Probajte nove boje + + Odaberite pozadinu koja vam odgovara. + + Istražite više pozadina + + + + Novi dodaci su sada dostupni + + Pogledajte preko 100 novih ekstenzija koje vam omogućuju da Firefox učinite svojim. + + Istražite dodatke + + + + Dodatak nije podržan + + Dodatak je već instaliran + + + + Dodaci su privremeno onemogućeni + + + Jedan ili više dodataka su prestali da rade, čineći vaš sistem nestabilnim. %1$s je neuspješno pokušao ponovo pokrenuti dodatak(e).\n\nDodaci se neće ponovo pokrenuti tokom vaše trenutne sesije.\n\nUklanjanje ili onemogućavanje dodataka može riješiti ovaj problem. + + Pokušajte ponovo pokrenuti dodatke + + Nastavite s onemogućenim dodacima + + + + Upravljanje računom + + Promijenite lozinku, upravljajte prikupljanjem podataka ili izbrišite svoj račun Sinhr. odmah @@ -487,6 +758,11 @@ and the third is the device model. --> %1$s na %2$s %3$s + + Kreditne kartice + + Adrese + Primljeni tabovi @@ -515,6 +791,16 @@ Marketinški podaci + + Dijeli osnovne podatke o korištenju s Adjustom, našim dobavljačem mobilnog marketinga + + Studije + + Dozvoljava Mozilla-i da instalira i pokrene studije + + + + Sinhronizujte i sačuvajte svoje podatke Prijavite se radi ponovnog povezivanja @@ -540,6 +826,11 @@ Slijedi temu uređaja + + + Povucite za osvježavanje + + Skrolujte da sakrijete alatnu traku Prevucite alatnu traku u stranu da promijenite tabove @@ -561,11 +852,31 @@ Historija + + Novi tab Postavke Zatvori + + Otvoriti %d tabova? + + Otvaranje ovoliko tabova može usporiti %s dok se stranice učitavaju. Jeste li sigurni da želite nastaviti? + + Otvori tabove + + Otkaži + + + %d stranica + + %d stranica + Nedavno zatvoreni tabovi @@ -579,6 +890,15 @@ Nema nedavno zatvorenih tabova + + + Tabovi + + Prikaz taba + + Lista + + Mreža Zatvori tabove @@ -590,27 +910,98 @@ Nakon mjesec dana + + Automatski zatvaraj otvorene tabove + + + + Ekran kod otvaranja aplikacije + + Početna stranica + + Zadnji tab + + Početna stranica nakon četiri sata neaktivnosti + + Zatvori ručno + + Zatvori nakon jednog dana + + Zatvori nakon jedne sedmice + + Zatvori nakon mjesec dana + + Otvori na početnoj stranici + + + Otvori u zadnjem tabu + + Otvori na početnoj stranici nakon četiri sata + + + + Premjestite stare tabove u neaktivne + + + Tabovi koje niste pogledali dvije sedmice se premještaju u neaktivan odjeljak. + + + + Ukloni + + Aktivno + + %1$s može s vremena na vrijeme instalirati i pokrenuti studije. + + Saznajte više + + Aplikacija će se zatvoriti radi primjene promjena + + OK + + Otkaži + + Napuštanje aplikacije radi primjene promjena… + Otvoreni tabovi Privatni tabovi + + Sinhronizovani tabovi Dodaj tab Dodaj privatni tab Privatni + + Sinhronizacija Podijeli sve tabove Nedavno zatvoreni tabovi + + Nedavno zatvoreni tabovi + + Postavke računa Postavke taba Zatvori sve tabove + + Oznaka + + Zatvori + + Podijeli odabrane tabove + + Meni odabranih tabova Ukloni tab iz kolekcije + + Odaberi tabove Zatvori tab @@ -625,6 +1016,10 @@ Preimenuj kolekciju Otvori tabove + + Naziv kolekcije + + Preimenuj Ukloni @@ -632,6 +1027,9 @@ %1$s (Privatni režim) + + + Unesi pojmove za pretragu Obriši historiju @@ -643,6 +1041,10 @@ %1$d označeno + + Danas + + Jučer Zadnjih 7 dana @@ -652,11 +1054,22 @@ Nema historije + + + Preuzimanja su uklonjena + + Uklonjeno %1$s + + Nema preuzetih fajlova %1$d označeno + + Ukloni + + Oprostite. %1$s ne može učitati stranicu. @@ -672,6 +1085,8 @@ Da li ste sigurni da želite obrisati ovaj direktorij? %s će obrisati označene stavke. + + Otkaži Dodaj direktorij @@ -688,6 +1103,10 @@ Otvori u novom tabu Otvori u privatnom tabu + + Otvori sve u novim tabovima + + Otvori sve u privatnim tabovima Obriši @@ -727,6 +1146,9 @@ VRATI + + Unesi pojmove za pretragu + Idi na Postavke @@ -738,8 +1160,16 @@ Preporučeno Obriši dozvole + + OK + + Otkaži Obriši dozvolu + + OK + + Otkaži Obriši dozvole na svim stranicama @@ -752,6 +1182,12 @@ Lokacija Obavještenje + + Trajno skladište + + Kolačići trećih strana + + DRM-kontrolisani sadržaj Pitaj za dozvolu @@ -764,8 +1200,16 @@ Izuzeci Isklj. + + Standardno + + Strogo + + Prilagođeno Dozvoli audio i video + + Dozvoli audio i video Blokiraj audio i video samo na mobilnom internetu @@ -773,13 +1217,22 @@ Blokiraj samo audio + + Blokiraj samo audio Blokiraj audio i video + + Blokiraj audio i video Uklj. Isklj. + + Uključeno + + Isključeno + Kolekcije @@ -820,6 +1273,11 @@ Prikaži + + OK + + Otkaži + Kolekcija %d @@ -829,15 +1287,33 @@ Podijeli + + Spasi kao PDF + + Nije moguće generisati PDF + + Odbaci + + Nije moguće štampati + + Nije moguće štampati ovu stranicu + + Štampaj Pošalji na uređaj Sve akcije Nedavno korišteno + + Kopiraj u međuspremnik + + Kopirano u međuspremnik Prijavite se na Sync - + + Sinhroniziraj i sačuvaj podatke + Pošalji na sve uređaje Ponovo se poveži na Sync @@ -864,6 +1340,39 @@ Zatvori privatne tabove + + Marketing + + Firefox je brz i privatan + + + Postavi Firefox kao zadani pretraživač + + Isprobaj privatno pretraživanje + + Pretražujte bez sačuvanih kolačića ili historije u %1$s + + Pretražujte bez praćenja + + Privatno pretraživanje u %1$su ne čuva vaše podatke. + + Započnite svoju prvu pretragu + + Pronađite nešto u blizini. Ili otkrijte nešto zabavno. + + + + Pomozite da poboljšamo Firefox tako što ćete ispuniti kratku anketu. + + Ispunite anketu + + Ne, hvala + Kolekcija obrisana @@ -873,10 +1382,16 @@ Tab zatvoren Tabovi zatvoreni + + Oznake sačuvane! + + Dodato u prečice! Privatni tab zatvoren Privatni tabovi zatvoreni + + Podaci privatnog pretraživanja su izbrisani VRATI @@ -888,6 +1403,10 @@ DOZVOLI ODBIJ + + Web adresa nije važeća. + + OK Da li ste sigurni da želite obrisati %1$s? @@ -925,6 +1444,8 @@ %d adresa + + Kolačići i podaci stranica Bit ćete odjavljeni na većini stranica @@ -933,6 +1454,8 @@ Oslobađa prostor za pohranu Dozvole stranica + + Preuzimanja Obriši podatke surfanja @@ -943,6 +1466,19 @@ Ugasi + + Vremenski raspon za brisanje + + Uklanja historiju (uključujući historiju sinhronizovanu s drugih uređaja), kolačiće i druge podatke o pretraživanju. + + Uklanja historiju (uključujući historiju sinhroniziranu s drugih uređaja) + + Zadnji sat + + Danas i juče + + Sve + %s će obrisati izabrane podatke surfanja. @@ -955,6 +1491,15 @@ Brišem podatke surfanja… + + Izbriši sve stranice u “%s” + + Otkaži + + Izbriši + + Grupa je izbrisana + Sync je uključen @@ -979,6 +1524,8 @@ Prijavite se pomoću kamere Radije koristi email + + Kreirajte ga da sinhronizujete Firefox između uređaja.]]> %s će zaustaviti sinhronizaciju s vašim računom, ali neće obrisati ništa od vaših podataka surfanja na ovom uređaju. @@ -994,15 +1541,23 @@ Postavke zaštite Napredna zaštita od praćenja + + Sada sadrži potpunu zaštitu od kolačića, našu najmoćniju barijeru do sada protiv elemenata za praćenje trećih strana. + + %s vas štiti od većine najčešćih pratilaca koji prate ono što radite online. Saznajte više Standardno (izvorno) + + Stranice će se normalno učitavati, ali će blokirati manje pratilaca. Šta je blokirano od strane standardne tracking zaštite Strogo + + Jača zaštita od praćenja i brže performanse, ali neke web stranice možda neće raditi ispravno. Šta je blokirano od strane stroge tracking zaštite @@ -1022,6 +1577,8 @@ Svi kolačići treće strane (može uzrokovati probleme s web stranicama) Svi kolačići (može uzrokovati probleme s web stranicama) + + Izolirajte kolačiće trećih strana Praćenje sadržaja @@ -1032,6 +1589,8 @@ Kriptomajneri Fingerprinteri + + Detalji Blokirano @@ -1042,8 +1601,12 @@ Ograničava mogućnost društvenih mreža da prate vaše aktivnosti surfanja na webu. Cross-Site Tracking kolačići + + Kolačići trećih strana Blokira kolačiće koje oglasne mreže i analitičke kompanije koriste kako bi prikupile podatke o vašem surfanju na mnogim web stranicama. + + Totalna zaštita od kolačića izolira kolačiće na web stranici na kojoj se nalazite tako da ih pratioci poput oglasnih mreža ne mogu koristiti da vas prate na svim web stranicama. Kriptomajneri @@ -1071,6 +1634,19 @@ The first parameter is the app name --> %s | OSS biblioteke + + Preusmjerite pratioce + + Briše kolačiće postavljene preusmjeravanjem na poznate web stranice za praćenje. + + Neki pratioci označeni u nastavku su djelimično deblokirani na ovoj stranici jer ste bili u interakciji s njima *. + + Saznajte više + + Ikona preferencije izuzetaka poboljšane zaštite od praćenja + Podrška @@ -1110,6 +1686,9 @@ Naziv prečice + + Ovu web stranicu možete lahko dodati na Početni ekran uređaja za brz pristup istoj i da surfate brže s iskustvom sličnom aplikaciji. + Prijave i lozinke @@ -1119,8 +1698,21 @@ Ne spašavaj nikad + + Automatski ispuni u %1$su + + Popunite i sačuvajte korisnička imena i lozinke na web stranicama dok koristite %1$s. + + Automatski ispuni u drugim aplikacijama + + Unesite korisnička imena i lozinke u druge aplikacije na svom uređaju. + + Dodaj prijavu + Sinkroniziraj prijave + + Sinhronizirajte prijave na svim uređajima Spašene prijave @@ -1150,8 +1742,14 @@ Korisničko ime kopirano u clipboard Kopiraj lozinku + + Očisti lozinku Kopiraj korisničko ime + + Očisti korisničko ime + + Očisti ime hosta Otvori stranicu u browseru @@ -1182,8 +1780,133 @@ Sortiraj meni prijava + + + Automatsko popunjavanje + + Adrese + + Kreditne kartice + + Sačuvajte i automatski popunjavajte kartice + + Podaci su šifrovani + + + Sinhronizujte kartice na svim uređajima + + Sinhronizuj kartice + + Dodaj kreditnu karticu + + Upravljaj sačuvanim karticama + + Dodaj adresu + + Upravljaj adresama + + Sačuvajte i automatski popunjavajte adrese + + Uključite informacije kao što su brojevi, e-mail i adrese za dostavu + + + Dodaj karticu + + Uredi karticu + + Broj kartice + + Datum isteka + + Mjesec isteka + + Godina isteka + + Ime na kartici + + Izbriši karticu + + Izbriši karticu + + Jeste li sigurni da želite izbrisati ovu kreditnu karticu? + + Izbriši + + Sačuvaj + + Sačuvaj + + Otkaži + + Sačuvane kartice + + Unesite važeći broj kreditne kartice + + Molimo popunite ovo polje + + Otključajte da vidite svoje sačuvane kartice + + Osigurajte svoje kreditne kartice + + Postavite obrazac za zaključavanje uređaja, PIN ili lozinku da zaštitite svoje sačuvane kreditne kartice od pristupa ako neko drugi ima vaš uređaj. + + Podesi odmah + + Kasnije + + Otključajte svoj uređaj + + Otključajte za korištenje sačuvanih podataka o kreditnoj kartici + + + Dodaj adresu + + Uredi adresu + + Upravljaj adresama + + Ime + + Srednje ime + + Prezime + + Adresa + + Grad + + Država + + Provincija + + Poštanski broj + + Država ili regija + + Telefon + + Email + + Sačuvaj + + Otkaži + + Izbriši adresu + + Jeste li sigurni da želite izbrisati ovu adresu? + + Izbriši + + Otkaži + + Sačuvaj adresu + + Izbriši adresu + Dodaj pretraživač + + Dodaj novi pretraživač Uredi pretraživač @@ -1197,16 +1920,33 @@ Ostalo + + Naziv Naziv + + Naziv pretraživača + + URL pojma za pretraživanje Tekst za pretragu + + URL koji će se koristiti za pretraživanje Zamijenite izraz sa “%s”. Primjer:\nhttps://www.google.ba/search?q=%s Detalji korisnički podešenog pretraživača + + API za prijedloge za pretraživanje (opcionalno) + + URL API prijedloga za pretraživanje + + Zamijenite upit sa “%s”. Primjer:\nhttps://suggestqueries.google.com/complete/search?client=firefox&q=%s + + Sačuvaj + Unesite naziv pretraživača @@ -1231,6 +1971,14 @@ %1$s na ON]]> + + Veza je sigurna + + Veza nije sigurna + + Izbrišite kolačiće i podatke o web stranici + + %s?]]> Da li ste sigurni da želite obrisati sve dozvole za sve stranice? @@ -1241,6 +1989,10 @@ Nema izuzetaka stranice Da li ste sigurni da želite obrisati ovu zabilješku? + + Dodaj u prečice + + Ukloni iz prečica Verifikovao: %1$s @@ -1252,6 +2004,8 @@ Da li ste sigurni da želite obrisati ovu prijavu? Obriši + + Otkaži Opcije prijave @@ -1265,8 +2019,14 @@ Uredi + + Dodaj novu prijavu Potrebna lozinka + + Korisničko ime je obavezno + + Ime hosta je obavezno Glasovna pretraga @@ -1274,6 +2034,13 @@ Prijava sa tim korisničkim imenom već postoji + + https://www.example.com + + Web adresa mora sadržavati "https://" ili "http://" + + Važeće ime hosta je obavezno + Povežite drugi uređaj @@ -1291,10 +2058,293 @@ Nema otvorenih tabova + + Proširi grupu sinhronizovanih tabova + + Sažmi grupu sinhroniziranih tabova + + + + Dostignuto je ograničenje prečice + + Da dodate novu prečicu, uklonite jednu. Dodirnite i držite stranicu i odaberite ukloni. OK, razumijem + + Prečice + + Naziv + + Naziv prečice + + OK + + Otkaži + + Postavke + + Naši sponzori i vaša privatnost + + Sponzorisano + + + + Neaktivni tabovi + + Zatvori sve neaktivne tabove + + Proširi neaktivne tabove + + Sažmi neaktivne tabove + + + + Automatsko zatvaranje nakon mjesec dana? + + Firefox može zatvoriti tabove koje niste pogledali u proteklom mjesecu. + + UKLJUČI AUTOMATSKO ZATVARANJE + + Automatsko zatvaranje omogućeno + + + + Firefox prijedlozi + + Google pretraga + + %s pretraga + + + Postavi automatsko otvaranje linkova web stranica, e-maila i poruka u Firefoxu. + Ukloni - + + Kliknite za više detalja + + + Idi prema gore + + + Zatvori + + + + Priče koje podstiču na razmišljanje + + Priče po temama + + Istražite više + + Pokreće %s. + + Dio porodice Firefox. %s + + Saznajte više + + Sponzorisano + + + Omogućite telemetriju za slanje podataka. + + Idi na postavke + + + + Provjera recenzije + + Provjera recenzije + + Pouzdane recenzije + + Kombinacija pouzdanih i nepouzdanih recenzija + + Nepouzdane recenzije + + Koliko su ove recenzije pouzdane? + + Prilagođena ocjena + + Nepouzdane recenzije su uklonjene + + Izdvajamo iz nedavnih recenzija + + Kako utvrđujemo kvalitet recenzija + + + Koristimo AI tehnologiju od %s od Mozille da provjerimo pouzdanost recenzija proizvoda. Ovo će vam samo pomoći da ocijenite kvalitetu recenzija, a ne kvalitet proizvoda. + + slovnu ocjenu od A do F.]]> + + Pouzdane recenzije. Vjerujemo da su recenzije vjerovatno od stvarnih kupaca koji su ostavili iskrene, nepristrasne recenzije. + + Vjerujemo da su recenzije pouzdane. + + Vjerujemo da postoji mješavina pouzdanih i nepouzdanih recenzija. + + Nepouzdane recenzije. Vjerujemo da su recenzije vjerovatno lažne ili od pristrasnih recenzenata. + + Vjerujemo da su recenzije nepouzdane. + + Prilagođena ocjena zasniva se samo na recenzijama za koje vjerujemo da su pouzdane.]]> + + + Istaknuto je iz %s recenzija u posljednjih 80 dana za koje vjerujemo da su pouzdane.]]> + + + Saznajte više o %su. + + kako %s od Mozilla određuje kvalitet recenzije + + kako %s određuje kvalitet recenzije + + Postavke + + Prikaži oglase u provjeri recenzije + + Povremeno ćete vidjeti oglase za relevantne proizvode. Svi oglasi moraju zadovoljiti naše standarde kvaliteta recenzije. %s + + Povremeno ćete vidjeti oglase za relevantne proizvode. Oglašavamo samo proizvode sa pouzdanim recenzijama. %s + + Saznajte više + + Isključite provjeru recenzije + + Više za razmatranje + + Oglas od %s + + Provjeru recenzije pokreće %s + + %s od Mozille + + + Nove informacije za provjeru + + Provjeri sada + + Još nema dovoljno recenzija + + Kada ovaj proizvod bude imao više recenzija, moći ćemo provjeriti njihov kvalitet. + + Proizvod nije dostupan + + Ako vidite da je ovaj proizvod ponovo na zalihama, prijavite ga i mi ćemo raditi na provjeri recenzija. + + Prijavite ovaj proizvod kada bude ponovo na zalihama + + Prijavite proizvod je na zalihama + + Provjera kvaliteta recenzije + + Provjera kvaliteta recenzije + + Ovo bi moglo potrajati oko 60 sekundi. + + Hvala na prijavi! + + Trebali bismo imati informacije o recenzijama ovog proizvoda u roku od 24 sata. Provjerite ponovo. + + Ne možemo provjeriti ove recenzije + + Nažalost, ne možemo provjeriti kvalitetu recenzija za određene vrste proizvoda. Na primjer, poklon kartice i streaming videa, muzike i igrica. + + Informacije stižu uskoro + + Trebali bismo imati informacije o recenzijama ovog proizvoda u roku od 24 sata. Provjerite ponovo. + + Analiza je ažurirana + + Razumijem + + Trenutno nema dostupnih informacija + + Radimo na rješavanju problema. Provjerite ponovo uskoro. + + Nema mrežne konekcije + + Provjerite svoju mrežnu konekciju, a zatim pokušajte ponovo učitati stranicu. + + Još uvijek nema informacija o ovim recenzijama + + Da biste saznali jesu li recenzije ovog proizvoda pouzdane, provjerite kvalitetu recenzija. Potrebno je samo oko 60 sekundi. + + Provjeri kvalitet recenzije + + Isprobajte naš pouzdani vodič za recenzije proizvoda + + Prije kupovine pogledajte koliko su pouzdane recenzije proizvoda na %1$s. Provjera recenzija, eksperimentalna funkcija od %2$s, ugrađena je direktno u pretraživač. Radi i na %3$s i %4$s. + + Prije kupovine pogledajte koliko su pouzdane recenzije proizvoda na %1$su. Provjera recenzija, eksperimentalna funkcija od %2$sa, ugrađena je direktno u pretraživač. + + Koristeći moć %1$s od strane Mozille, pomažemo vam da izbjegnete pristrasne i neautentične recenzije. Naš AI model se uvijek poboljšava kako bi vas zaštitio dok kupujete. %2$s + + Saznajte više + + Odabirom “Da, probaj” prihvatate %1$s od strane Mozilla-ine %2$s i %3$s. + + Odabirom “Da, probaj” prihvatate sljedeće od %1$s: + + politika privatnosti + + Politika privatnosti + + uslovi korištenja + + Uslovi korištenja + + Da, probaj + + Ne sada + + Saznajte možete li vjerovati recenzijama ovog proizvoda — prije nego što kupite. + + Pokušaj provjeriti recenziju + + Jesu li ove recenzije pouzdane? Provjerite sada da vidite prilagođenu ocjenu. + + Otvori provjeru recenzije + + Beta + + Otvori provjeru recenzije + + Zatvori provjeru recenzije + + %1$s od 5 zvjezdica + + Prikaži manje + + Prikaži više + + Kvalitet + + Cijena + + Dostava + + Pakovanje i izgled + + Konkurentnost + + + + sažmi + + sažeto + + proširi + + prošireno + + otvorite link da saznate više o ovoj kolekciji + + pročitaj članak + + otvorite link da saznate više + + %s, naslov + diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index d820b2f819..35d7578de9 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -73,6 +73,11 @@ The first parameter is the name of the app defined in app_name (for example: Firefox Nightly) The second parameter is the clickable link text in felt_privacy_info_card_subtitle_link_text --> El %1$s suprimeix les galetes, l’historial i les dades dels llocs en tancar totes les finestres privades. %2$s + + El %1$s suprimeix les galetes, l’historial i les dades dels llocs en tancar totes les perstanyes privades. %2$s @@ -201,6 +206,8 @@ Biblioteca Lloc d’escriptori + + Obre en una pestanya normal Afegeix a la pantalla d’inici @@ -258,7 +265,7 @@ Escaneja - Motor de cerca + Motor de cerca Paràmetres del motor de cerca @@ -327,9 +334,13 @@ Feu que el Firefox sigui el vostre navegador per defecte + + Ens encanta mantenir-vos segur El Firefox prioritza les persones sobre els beneficis i defensa la vostra privadesa blocant els elements de seguiment entre llocs.\n\nTrobareu més informació en l’avís de privadesa. + + El nostre navegador sense ànim de lucre ajuda a evitar que les empreses us segueixin en secret al web.\n\nObteniu més informació al nostre avís de privadesa. avís de privadesa @@ -340,8 +351,13 @@ Ara no Salteu del telèfon al portàtil, i viceversa + + Manteniu el xifrat quan passeu d\'un aparell a un altre Recupereu les pestanyes i les contrasenyes dels altres dispositius per continuar des d’on ho havíeu deixat. + + Si teniu la sessió iniciada i sincronitzada, esteu més segur. Firefox xifra les contrasenyes, els marcadors i molt més. Inicia la sessió @@ -349,9 +365,15 @@ Les notificacions us ajuden a fer més coses amb el Firefox + + Les notificacions us ajuden a estar més segur amb el Firefox Envieu pestanyes entre dispositius, gestioneu les baixades i obteniu consells per a treure el màxim profit del Firefox. + + Envieu pestanyes de manera segura entre els vostres dispositius i descobriu altres funcions de privadesa al Firefox. Activa les notificacions @@ -391,6 +413,8 @@ Trieu-ne un Gestioneu les dreceres de cerca + + Gestiona els motors de cerca alternatius Editeu els motors visibles al menú de cerca @@ -401,10 +425,18 @@ Cerca Motors de cerca + + Suggeriments dels motors de cerca Barra d’adreces + + Preferències de la barra d\'adreces + + Barra d\'adreces - Suggeriments del Firefox + + Més informació sobre els suggeriments del Firefox Valora en el Google Play - Reducció de bàners de galetes + Reducció de bàners de galetes + + Blocador de bàners de galetes + + Blocador de bàners de galetes en la navegació privada - Redueix els bàners de galetes + Redueix els bàners de galetes - Desactivada + Desactivada - Activada + Activada - El %1$s intenta rebutjar automàticament les sol·licituds de galetes que es mostren com a bàners de galetes. + El %1$s intenta rebutjar automàticament les sol·licituds de galetes que es mostren com a bàners de galetes. Desactivada en aquest lloc @@ -459,27 +495,40 @@ Aquest lloc no és compatible ara per ara - Voleu activar la reducció de bàners de galetes per a %1$s? + Voleu activar la reducció de bàners de galetes per a %1$s? + + Voleu activar el blocador de bàners de galetes per a %1$s? + + Voleu desactivar la reducció de bàners de galetes per a %1$s? - Voleu desactivar la reducció de bàners de galetes per a %1$s? + Voleu desactivar el blocador de bàners de galetes per a %1$s? El %1$s no pot rebutjar automàticament les sol·licituds de galetes d’aquest lloc. Podeu enviar una sol·licitud per a fer que aquest lloc sigui compatible en el futur. - El %1$s esborrarà les galetes d’aquest lloc i actualitzarà la pàgina. En esborrar totes les galetes, pot ser que se us tanquin les sessions o que se us buidin els carros de la compra. + El %1$s esborrarà les galetes d’aquest lloc i actualitzarà la pàgina. En esborrar totes les galetes, pot ser que se us tanquin les sessions o que se us buidin els carros de la compra. + + Si el desactiveu, el %1$s esborrarà les galetes i tornarà a carregar aquest lloc. Això pot fer que se us tanquin les sessions o que se us buidin els carros de la compra. + + El %1$s intenta rebutjar automàticament totes les sol·licituds de galetes de tots els llocs compatibles. - El %1$s intenta rebutjar automàticament totes les sol·licituds de galetes de tots els llocs compatibles. + Si l\'activeu, el %1$s intentarà rebutjar automàticament tots els bàners de galetes d\'aquest lloc. - Voleu el %1$s rebutgi els bàners de galetes? + Voleu el %1$s rebutgi els bàners de galetes? - El %1$s pot rebutjar automàticament moltes sol·licituds de bàners de galetes. + El %1$s pot rebutjar automàticament moltes sol·licituds de bàners de galetes. - Ara no + Ara no - Veureu menys sol·licituds de galetes + Veureu menys sol·licituds de galetes - Permet + Permet + + + El %1$s ha rebutjat les galetes + + Menys distraccions, menys galetes que us fan el seguiment en aquest lloc. Intenta connectar-se als llocs mitjançant el protocol de xifratge HTTPS automàticament per millorar la seguretat. @@ -590,6 +639,8 @@ Complements + + Instal·la un complement des d\'un fitxer Notificacions @@ -650,16 +701,10 @@ Més informació %s clàssic - - Edició limitada Sèrie d\'artista - - La nova col·lecció Veus independents. %s La col·lecció Veus independents. %s - - La nova col·lecció Veus independents. La col·lecció Veus independents. @@ -669,6 +714,14 @@ Descobriu més fons de pantalla + + + Nous complements disponibles + + Descobriu les més de cent noves extensions que us permeten fer el Firefox vostre. + + Explora els complements + El complement no és compatible @@ -1410,14 +1463,11 @@ %d pestanyes - Historial de navegació i dades dels llocs Historial de navegació %d adreces - - Galetes Galetes i dades dels llocs @@ -1476,62 +1526,10 @@ S’ha suprimit el grup - - Us donem la benvinguda a una Internet millor - - Un navegador creat per a les persones i no per al lucre. - - Continueu des d’on ho deixeu - - Sincronitzeu les pestanyes i les contrasenyes entre dispositius per passar d’una pantalla a una altra de forma transparent. - - Inicia la sessió La sincronització està activada - - Protecció de la privadesa per defecte - - El %1$s bloca automàticament les empreses que, en secret, us fan el seguiment mentre navegueu. - - La protecció total de galetes impedeix que els seguidors utilitzin les galetes per a perseguir-vos d’un lloc web a l’altre. - - Estàndard (per defecte) - - Equilibri entre privadesa i rendiment. Les pàgines es carreguen amb normalitat. - - Estricta - - Bloca més elements de seguiment i fa que les pàgines es carreguin més ràpidament, però podria causar problemes amb algunes funcions de la pàgina. - - Trieu la ubicació de la barra d’eines - - Manteniu-la a la part inferior o moveu-la a la part superior. - - Teniu el control de les vostres dades - - El Firefox us dona el control sobre tot allò que compartiu en línia i que compartiu amb nosaltres. - - Mostra l’avís de privadesa - - - A punt per obrir una Internet fascinant? - - Comença a navegar - - Trieu el tema - - Estalvieu bateria i descanseu la vista amb el mode fosc. - - Automàtic - - S’adapta als paràmetres del dispositiu - - Tema fosc - - Tema clar - S’han enviat les pestanyes @@ -1974,8 +1972,6 @@ URL de l’API de suggeriments de cerca - Substituïu la consulta per «%s». Per exemple:\nhttp://suggestqueries.google.com/complete/search?client=firefox&q=%s - Substituïu la consulta per «%s». Per exemple:\nhttps://suggestqueries.google.com/complete/search?client=firefox&q=%s Desa @@ -2205,7 +2201,7 @@ Ressenyes poc fiables - Són fiables aquestes revisions? + Són fiables aquestes ressenyes? Valoració ajustada @@ -2215,6 +2211,142 @@ Com es determina la qualitat de la ressenya + + Usem tecnologia d\'IA de %s de Mozilla per a comprovar la fiabilitat de les ressenyes de productes. Això només us ajudarà a avaluar la qualitat de la ressenya, no la qualitat del producte. + + puntuació alfabètica de la A a la F.]]> + + Les ressenyes són fiables. Pensem que probablement són de clients reals honestos i no estan esbiaixades. + + Creiem que les ressenyes són fiables. + + Creiem que hi ha una barreja de ressenyes fiables i poc fiables. + + Les ressenyes són poc fiables. Creiem que poden ser falses o esbiaixades. + + Creiem que les ressenyes són poc fiables. + + puntuació ajustada només es basa en les ressenyes que creiem fiables.]]> + + destacats provenen de %s ressenyes dels darrers 80 dies que creiem que són fiables.]]> + + Més informació sobre %s. + + com %s de Mozilla determina la qualitat de la ressenya + + com determina %s la qualitat de la ressenya + + Paràmetres + + Mostra anuncis al verificador de ressenyes + + Veureu anuncis ocasionals de productes rellevants. Tots els anuncis han de complir els nostres estàndards de qualitat de revisió. %s + + Veureu anuncis ocasionals de productes rellevants. Només anunciem productes amb ressenyes fiables. %s + + Més informació + + Desactiva el verificador de ressenyes + + Més productes a tenir en compte + + Anunci de %s + + El verificador de ressenyes funciona amb tecnologia de %s + + %s de Mozilla + + Nova informació per a comprovar + + Comprova-ho ara + + Encara no hi ha prou ressenyes + + Quan aquest producte tingui més ressenyes, podrem comprovar-ne la qualitat. + + El producte no està disponible + + Si veieu que torna a haver-hi estoc del producte, informeu-nos i en comprovarem les ressenyes. + + Informa que torna a haver-hi estoc del producte + + Informa que hi ha estoc del producte + + S\'està comprovant la qualitat de la ressenya + + S\'està comprovant la qualitat de la ressenya + + Això pot trigar uns 60 segons. + + Gràcies per informar-ne! + + Hauríem de tenir informació sobre les ressenyes del producte d\'aquí a 24 hores. Torneu més tard. + + No podem comprovar aquestes ressenyes + + Malauradament, no podem comprovar la qualitat de les ressenyes per certs tipus de productes. Per exemple, targetes de regal, transmissions de vídeo, música i jocs. + + Informació disponible pròximament + + Hauríem de tenir informació sobre les ressenyes del producte d\'aquí a 24 hores. Torneu més tard. + + L\'anàlisi està actualitzat + + Entesos + + No hi ha informació disponible ara mateix + + Estem treballant per a resoldre el problema. Torneu-hi més tard. + + No hi ha connexió de xarxa + + Comproveu la connexió de xarxa i torneu a carregar la pàgina. + + Encara no hi ha informació sobre aquestes ressenyes + + Per saber si les ressenyes són fiables, comproveu-ne la qualitat. Només són 60 segons. + + Comprova la qualitat de les ressenyes + + Proveu la nostra guia de confiança per a ressenyes de productes + + Vegeu com de fiables són les ressenyes de productes a %1$s abans de comprar. El verificador de ressenyes, una funció experimental de %2$s, està integrat directament al navegador. També funciona a %3$s i %4$s. + + Vegeu com de fiables són les ressenyes de productes a %1$s abans de comprar. El verificador de ressenyes, una funció experimental de %2$s, està integrada directament en el navegador. + + Amb el poder de %1$s de Mozilla, us ajudem a evitar ressenyes esbiaixades i no autèntiques. El nostre model d\'IA sempre millora per a protegir-vos mentre compreu. %2$s + + Més informació + + En seleccionar «Sí, prova-ho», accepteu %1$s de %2$s i %3$s de Mozilla. + + En seleccionar «Sí, prova-ho», accepteu les polítiques i condicions següents de %1$s: + + política de privadesa + + Política de privadesa + + condicions d\'ús + + Condicions d\'ús + + Sí, prova-ho + + Ara no + + Esbrineu si podeu confiar en les ressenyes d\'aquest producte abans de comprar-lo. + + Prova el verificador de ressenyes + + Són fiables aquestes ressenyes? Comproveu-ho ara per a veure una puntuació ajustada. + + Obre el verificador de ressenyes + + Beta + + Obre el verificador de ressenyes + + Tanca el verificador de ressenyes %1$s de 5 estrelles @@ -2235,12 +2367,18 @@ reduir + + reduït ampliar + + expandit obrir l’enllaç per obtenir més informació sobre aquesta col·lecció llegir l’article obrir l’enllaç i veure més informació + + %s, encapçalament diff --git a/app/src/main/res/values-cak/strings.xml b/app/src/main/res/values-cak/strings.xml index b29e563263..bcf9f28fb2 100644 --- a/app/src/main/res/values-cak/strings.xml +++ b/app/src/main/res/values-cak/strings.xml @@ -68,6 +68,24 @@ %1$s nuyüj ri akanoxik chuqa\' runatab\'al awokem pa k\'amaya\'l toq yatel pa ri chokoy o ri ichinan taq wi\'. Estape\' re re\' man yatrewaj ta pan ajk\'amaya\'l o chuwäch ri niya\'on ak\'amaya\'l, yatruto\' richin nichinäx ronojel ri nab\'än pan k\'amab\'ey chuwäch jun chik winäq nrokisaj ri awokisab\'al. Ch\'ob\'on taq tzij chi rij ri ichinan okem pa k\'amaya\'l + + + Man taya\' kan awetal pa re okisaxel re\' + + %1$s yeruyüj ri taq acookie, natab\'äl chuqa\' taq rutzij ruxaq k\'amaya\'l toq ye\'atz\'apij ronojel ri ichinan taq atzuwäch. %2$s + + %1$s yeruyüj ri taq acookie, natab\'äl chuqa\' taq rutzij ruxaq k\'amaya\'l toq ye\'atz\'apij ronojel ri ichinan taq awi\'. %2$s + + ¿Achike taq k\'a nitz\'eton ri nasamajij? + Tijaq ri jun chik awichinan ruwi\' rik\'in xa jun xak. @@ -91,6 +109,11 @@ Tetamäx ch\'aqa\' chik chi rij ri Kichajixik Chi Jun Kuki + + + Tapitz\'a\' wawe\' richin natikirisaj jun k\'ak\'a\' ichinan moloj. Ke\'ayuju\' ri anatab\'al, taq acookie — ronojel. + + Najowäx okem pa elesäy wachib\'äl. Jät pa runuk\'ulem Android, tachapa\' pa ya\'oj q\'ij k\'a ri\' pa tiya\' q\'ij. @@ -187,6 +210,8 @@ Wujb\'äl Ruxaq ch\'atal + + Tijaq pa jun choj ruwi\' Titz\'aqatisäx pa ri Rutikirib\'al ruwa @@ -230,6 +255,9 @@ Rutikirib\'al ruwäch + + Tiyuj runatab\'al okem pa k\'amaya\'l Xcha\' ch\'ab\'äl @@ -242,7 +270,7 @@ Tiwachib\'ëx - Kanob\'äl + Kanob\'äl Runuk\'ulem kanob\'äl @@ -257,7 +285,7 @@ %s xtukomonij ronojel ri xtatz\'ib\'aj pa ri kikajtz\'ik taq ochochib\'äl rik\'in ri akanob\'al k\'o wi. - + Tikanöx %s @@ -268,6 +296,9 @@ Wakami tikanöx pa: + + %s okik\'amaya\'l + Tawetamaj ruwäch ri ichinan ruxaq tikirib\'äl. Ri k\'ak\'a\' taq ruwi\', taq yaketal chuqa\' retal taq kanoxïk wawe\' xkeq\'alajin. @@ -303,38 +334,51 @@ Wakami mani - - - Tab\'ana\' chi ri %s tok jeb\'ël awokik\'amaya\'l + - Tab\'ana\' chi ri Firefox tok jeb\'ël awokik\'amaya\'l + Tab\'ana\' chi ri Firefox tok jeb\'ël awokik\'amaya\'l + + Niqa chi qawäch chi atqajikib\'an + + Firefox nuya\' kiq\'ij ri winaqi\' chuwäch ri ch\'akoj chuqa\' nuchajij ri awichinanem rik\'in yeruq\'ät ri taq ojqanela\' chi kikojol taq ruxaq k\'amaya\'l.\n\nCh\'aqa\' chik taq rutzijol pa ri rutzijol ichinanem. - ichinan na\'oj + ichinan na\'oj - Tiya\' kan achi\'el kanob\'äl k\'o wi + Tiya\' kan achi\'el kanob\'äl k\'o wi - Wakami mani + Wakami mani - Tajala\' awoyonib\'al rik\'in akematz\'ib\' o pa k\'exoj + Tajala\' awoyonib\'al rik\'in akematz\'ib\' o pa k\'exoj - Ke\'akolo\' ri taq ruwi\' o ewan taq tzij pa ri ch\'aqa\' chik taq awokisaxel richin nasamajij chik el akuchi\' xakanäj kan. + Ke\'akolo\' ri taq ruwi\' o ewan taq tzij pa ri ch\'aqa\' chik taq awokisaxel richin nasamajij chik el akuchi\' xakanäj kan. - Titikirisäx molojri\'ïl + Titikirisäx molojri\'ïl - Wakami mani - - Ri taq rutzijol yatkito\' richin nasamajij ch\'aqa\' chik pa %s + Wakami mani - Ri taq rutzijol yatkito\' richin nasamajij ch\'aqa\' chik pa Firefox + Ri taq rutzijol yatkito\' richin nasamajij ch\'aqa\' chik pa Firefox + + Ri taq rutzijol yatkito\' yalan at jikil pa Firefox + + Ketzij taq rutzijol - Wakami mani + Wakami mani + + + Tatojtob\'ej ri ruwidget rukanob\'äl Firefox + + Titz\'aqatisäx ruwidget Firefox + + Wakami mani @@ -355,12 +399,32 @@ Tawetamaj qawäch + + Tacha\' jun + + Ke\'asamajij taq ruq\'a\' kanoxïk + + Ke\'asamajij cha\'el taq kanob\'äl + + Kenuk\' tz\'etel taq kanob\'äl pa ruch\'a\'on samaj kanob\'äl + + Tz\'etel taq kanob\'äl pa ri rucha\'on samaj kanob\'äl Kanob\'äl k\'o wi Tikanöx + + Ttaq kanob\'äl + + Chilab\'en taq kanob\'äl - Kikajtz\'ik Ochochib\'äl + Kikajtz\'ik Ochochib\'äl + + Kajowab\'al kikajtz\'ik ochochib\'äl + + Kikajtz\'ik ochochib\'äl - Ruchilab\'exik Firefox + + Tawetamaj ch\'aqa\' chik chi rij ri Firefox Suggest Tiya\' rejqalem pa Google Play - Kech\'utinirisäx kitzijol kuki + Kech\'utinirisäx kitzijol kuki + + Kiq\'atöy kitzijol cookie + + Ruq\'atöy kitzijol taq cookie pan ichinan okem - Kech\'utinär ri taq kib\'aner koki + Kech\'utinär ri taq kib\'aner koki - Tichup + Tichup - Titzij + Titzij - %1$s nutojtob\'ej yeruxutuj ruyonil ri kik\'utuxik kuki pa ri taq kinimawuj kuki. + %1$s nutojtob\'ej yeruxutuj ruyonil ri kik\'utuxik kuki pa ri taq kinimawuj kuki. Chupül pa re ruxaq re\' Tiq\'at - Tik\'utüx to\'ïk - Titaq k\'utuj - Kech\'utinirisäx kitzijol kuki + ¿Tik\'utüx to\'ïk richin re ruxaq re\'? + + Xtaq k\'utuj Tzijïl pa re ruxaq re\' + + Xtaq ruk\'utuj to\'ïk Wakami man koch\'el ta re ruxaq re\' - ¿La nitzij ri kich\'utinisaxik rutzijol kuki richin %1$s? + ¿La nitzij ri kich\'utinisaxik rutzijol kuki richin %1$s? + + ¿La nitzij ri kiq\'atik taq kitzijol cookie richin %1$s? - ¿La nichup ri kich\'utinisaxik rutzijol kuki richin %1$s? + ¿La nichup ri kich\'utinisaxik rutzijol kuki richin %1$s? + + ¿La nichup ri ruq\'atoj kitzijol taq cookie richin %1$s? + + Man yeruq\'ät ta ri %1$s pa ruyonil ri taq kik\'utuj taq cookie pa re ruxaq re\'. Yatikïr natäq un k\'utuj richin nito\' re ruxaq re\' ri chwa\'q kab\'ij. - %1$s xkeruyüj ri taq rukuki re ruxaq chuqa\' xtuk\'ëx re ruxaq. Xkeruyüj ronojel ri taq kuki, nitikïr nutz\'apij ri molojri\'ïl o yerujäm ri taq ruch\'ich\' loq\'oj. + %1$s xkeruyüj ri taq rukuki re ruxaq chuqa\' xtuk\'ëx re ruxaq. Xkeruyüj ronojel ri taq kuki, nitikïr nutz\'apij ri molojri\'ïl o yerujäm ri taq ruch\'ich\' loq\'oj. - %1$s nitikïr nutojtob\'ej yeruxutuj pa ruyonil ronojel ri kik\'utuxik taq kuki pa koch\'el taq ruxaq. + %1$s nitikïr nutojtob\'ej yeruxutuj pa ruyonil ronojel ri kik\'utuxik taq kuki pa koch\'el taq ruxaq. - ¿La niya\' q\'ij chi %1$s yeruxutuj ri kik\'utuxik taq kuki? + ¿La niya\' q\'ij chi %1$s yeruxutuj ri kik\'utuxik taq kuki? - %1$s nitikïr yeruxutuj pa ruyonil k\'ïy kik\'utuxik taq kuki. + %1$s nitikïr yeruxutuj pa ruyonil k\'ïy kik\'utuxik taq kuki. - Wakami mani + Wakami mani - Xtatz\'ët jub\'a kik\'utuxik kuki + Xtatz\'ët jub\'a kik\'utuxik kuki - Tiya\' q\'ij + Tiya\' q\'ij + + + %1$s k\'a b\'a\' keruxutuj taq cookie + + Jub\'a\' nikik\'äm atzub\'al, jub\'a\' taq cookie ri yatkojqaj pa re ruxaq re\'. Tichup @@ -454,11 +535,11 @@ Okel - Ichinan ruk\'u\'x rusamaj Firefox Account + Ichinan ruk\'u\'x rusamaj Firefox Account Ichinan ruk\'u\'x rusamaj Sync - Xjal ruk\'u\'x kisamaj Firefox Account/Sync. Nitz\'apïx chokoy richin yesamajïx ri taq jaloj… + Xjal ruk\'u\'x kisamaj Firefox Account/Sync. Nitz\'apïx chokoy richin yesamajïx ri taq jaloj… Rub\'i\' taqoya\'l @@ -474,8 +555,10 @@ Tatikirisaj molojri\'ïl richin ye\'axïm taq ruwi\', taq yaketal, ewan tzij chuqa\' juley chik. - Firefox Taqoya\'l + Firefox Taqoya\'l + + Rub\'i\' rutaqoya\'l Mozilla Tokisäx chik richin nitikirisäx chik ri ximoj @@ -487,7 +570,7 @@ Näj chojmirisanem pa USB - Tik\'ut kanob\'äl + Tik\'ut kanob\'äl Kek\'ut pe ri taq chilab\'enïk richin yakanon @@ -508,6 +591,13 @@ Kinuk\'ulem Rub\'i\' Taqoya\'l Titz\'aqatisäx ruyon URLs + + Chilab\'en taq to\'onela\' + + Taq ruchilab\'exik %1$s + + Kek\'ul taq ruchilab\'exik web ruximon ri\' rik\'in ritaq akanoxik Kejaq taq ximonel pa taq chokoy @@ -518,6 +608,12 @@ Majub\'ey Runuk\'samajel chi rij qasanïk + + + Titzij taq rutz\'ib\'axik Gecko + + Tajin nel pa ri okisanel richin yejikib\'äx taq jaloj… + Taq tz’aqat @@ -581,12 +677,6 @@ Tetamäx ch\'aqa\' chik Ojer %s - - Jub\'a\' Ruwäch - - Ri k\'ak\'a\' Jamäl ch\'ab\'äl molb\'äl. %s - - Ri k\'ak\'a\' Jamäl ch\'ab\'äl molb\'äl. Tatojtob\'ej jun rub\'onil pa rub\'eyal @@ -595,13 +685,34 @@ Kekanöx ch\'aqa\' rupam ruwäch + + + K\'ak\'a\' taq tz\'aqa ewachel wakami + + Ke\'awila\' +100 k\'ak\'a\' taq k\'amal ri nikiya\' q\'ij chawe\' nawichinaj Firefox. + + Kenik\'öx taq tz\'aqat + - Man nuk\'äm ta ri\' ri tz\'aqat + Man nuk\'äm ta ri\' ri tz\'aqat - Xyak ri tz\'aqat + Xyak ri tz\'aqat + + + + Echupun jumej ri taq tz\'aqat + + Tatojtob\'ej ye\'atzïj chik ri taq tz\'aqat + + + Kesamajïx chik ri chupun taq tz\'aqat + + Tinuk’samajïx rub‘i\' taqoya\'l + + Tajala\' ri ewan atzij, tanuk\'samajij ri kik\'olik taq tzij o tiyuj ri rub\'i\' taqoya\'l Tixim wakami @@ -791,6 +902,9 @@ Jun ik\' tik\'o + + Ketz\'apïx kiyon ri jaqäl taq ruwi\' + Ruxaq tikirib\'äl @@ -804,9 +918,20 @@ Titz\'apïx chi rij jun ik\' + + Tijaq pa tikirib\'al ruxaq Tijaq pa ruk\'isib\'äl ruwi\' + + Tijaq pa tikirib\'äl ruxaq pa kaji\' ramaj + + + + Kejal ri ojer taq ruwi\' pa echupun + + Ri taq ruwi\' man e\'atz\'eton ta pa ka\'i\' wuqq\'ij yejal pa ri echupun tanaj. + Tiyuj @@ -816,11 +941,16 @@ %1$s k\'o b\'ey nitikïr yeruyäk chuqa\' yerub\'än taq tijob\'alil. Tetamäx ch\'aqa\' chik + + Xtitz\'apitäj ri okisanel richin yejikib\'äx ri taq jaloj ÜTZ Tiq\'at + + Tajin nel pa ri okisanel richin yejikib\'äx taq jaloj… + Kejaq taq ruwi\' @@ -877,9 +1007,9 @@ Rub\'i\' mol - Tisik\'ïx chik - - Tiyuj + Tisik\'ïx chik + + Tiyuj Tiyuj pa natab\'äl @@ -1155,8 +1285,16 @@ Tikomonïx Tiyak achi\'el PDF - + Man xtz\'uk ta ri PDF + + Tewäx + + Man tikirel ta xtz\'ajb\'äx + + Man tikirel ta nitz\'ajb\'äx re ruxaq re\' + + Titz\'ajb\'äx Titaq pa okisab\'äl @@ -1222,6 +1360,11 @@ Tatikirisaj ri nab\'ey akanoxik + + Tinojisäx nojwuj + + Mani matyox + Xyuj ri mol @@ -1233,11 +1376,15 @@ Taq ruwi\' xetz\'apïx ¡Xeyak taq yaketal! + + ¡Xtz\'aqatisäx pa ruq\'a\'! Ichinan ruwi\' xtz\'apïx Ichinan taq ruwi\' xetz\'apïx + + Xeyuj ri taq rutzij ichinan okem TITZOLÏX @@ -1249,6 +1396,8 @@ TIYA\' Q\'IJ MAN TIYA\' Q\'IJ + + Man okel ta web ochochib\'äl. ÜTZ @@ -1284,12 +1433,13 @@ %d taq ruwi\' - Runatab\'al okem chuqa\' taq rutzij ruxaq k\'amaya\'l + + Runatab\'al okem %d taq ochochib\'äl - - Taq kuki + + Taq cookie chuqa\' taq rutzij ruxaq Xketz\'apïx molojri\'ïl pa b\'ama ronojel ruxaq @@ -1310,6 +1460,8 @@ Tel + + Rupajb\'al ramaj richin niyuj Ruk\'isib\'äl ramaj @@ -1329,6 +1481,8 @@ yeyuj taq rutzij okem pa k\'amaya\'l… + + Keyuj ronojel ri taq ruxaq pa “%s” Tiq\'at @@ -1336,51 +1490,11 @@ Xyuj molaj - - Tasik\'a\' el akuchi\' xya\' wi kan - - Kek\'am pe taq yaketal, natab\'äl chuqa\' ewan taq tzij pa Firefox pa re okisab\'äl re\'. - - Titikirisäx molojri\'ïl + Tzijïl Sync - - Ruk\'amon pe ruchajinem awichinanem - - %1$s ruyonil yeruq\'ät ri ajk\'ayij moloj yatkojqaj pan ewäl pan ajk\'amaya\'l. - - Junaman (k\'o wi) - - Junaman richin ichinanem chuqa\' rub\'eyal nisamäj. Yesamajïx ri taq ruxaq pa relik rub\'eyal. - - Nimaläj - - Yeruq\'ät mas ojqanela\' richin anin yesamäj ri taq ruxaq, po rik\'in jub\'a\' jujun taq rusamaj re ruxaq xkesachiyaj. - - Tacha\' ri ruk\'ojlib\'al rukajtz\'ik samajib\'äl - - - Tasik\'ij ri rutzijol qichinanem - - - Titikirisäx okem pa k\'amaya\'l - - - Tacha\' ri awachinel - - Takolo\' jub\'a\' ruwateriya\' chuqa\' tachajij ri runaq\' awäch, tacha\' ri q\'ëq rub\'anikil. - - Yonil - - - Nuk\'äm ri\' rik\'in ri runuk\'ulem awoyonib\'al - - Q\'equ\'m wachinel - - Säq wachinel - ¡Xetaq taq ruwi\'! @@ -1416,23 +1530,19 @@ Runuk\'ulem Ojqanem Utzirisan Chajinïk chuwäch Ojqanem - - Katok pa k\'amaya\'l akuchi\' man yatoqäx ta - - Yeruchajij ri taq atzij. %s yaruchajij chi kiwäch relik ojqanela\', ri nikoqaj ri nab\'än pa k\'amab\'ey. Tetamäx ch\'aqa\' chik Junaman (k\'o wi) - Junaman richin ichinanem chuqa\' rub\'eyal nisamäj. Yesamajïx ri taq ruxaq pa relik rub\'eyal. + Xkesamäj ri taq ruxaq achi\'el rub\'anon, xa xe chi man k\'ïy ta taq ojqanela\' xkeq\'at. Achike ri q\'aton ruma ri relik ruchajixik ojqanem Nimaläj - Yeruq\'ät mas ojqanela\' richin anin yesamäj ri taq ruxaq, po rik\'in jub\'a\' jujun taq rusamaj re ruxaq xkesachiyaj. + K\'ïy chajinïk chuwäch ojqanem chuqa\' ütz rub\'eyal nisamäj, xa xe chi jujun taq ruxaq rik\'in jub\'a\' man xkesamäj ta ütz. Achike ri q\'aton ruma ri ruchajixik ojqanem k\'o @@ -1452,6 +1562,8 @@ Konojel ri taq kikuki aj rox winäq (yetikïr yetz\'ilon pan ajk\'amaya\'l ruxaq) Ronojel kuki (xketz\'ilon pa ri ajk\'amaya\'l ruxaq) + + Kejech\'üx taq cookie pa xoch\'in taq ruxaq Rupam ojqanem @@ -1572,6 +1684,10 @@ Majub\'ey tiyak + + Tinojisäx ruyonil pa %1$s + + Titz\'aqatisäx ruyon pa ch\'aqa\' chik taq okisab\'äl. Tatz\'aqatisaj rutikirib\'al molojri\'ïl @@ -1753,6 +1869,10 @@ Tiyak Tiq\'at + + Keyuj ochochib\'äl + + ¿La at jikil chi nawajo\' nayüj re ochochib\'äl re\'? Tiyuj @@ -1764,30 +1884,43 @@ Titz\'aqatisäx kanob\'äl + + Titz\'aqatiäx k\'ak\'a\' kanob\'äl Tinuk\' kanob\'äl - Titz\'aqatisäx + Titz\'aqatisäx - Tiyak + Tiyak Tinuk\' Tiyuj - Juley chik + Juley chik + + B\'i\'aj - B\'i\'aj + B\'i\'aj + + Rub\'i\' ri kanob\'äl + + Cholajem rukanoxik URL - Rucholajil kanoxïk nokisäx + Rucholajil kanoxïk nokisäx + + URL xtokisäx richin ri kanoxïk Tik\'ex k\'ulb\'enïk rik\'in “%s”. Achi\'el: \nhttps://www.google.com/search?q=%s Rub\'anikil ichinan kanob\'äl + + Tiyak + Titz\'ib\'äx rub\'i\' kanob\'äl @@ -1829,6 +1962,10 @@ Achi\'el: \nhttps://www.google.com/search?q=%s Majun man relik ta richin re ruxaq ¿La kan nawajo\' chi niyuj el re yaketal re\'? + + Titz\'aqatisäx ruq\'a\' + + Tiyu pa ruq\'a\' Jikib\'an Ruma: %1$s @@ -1854,8 +1991,14 @@ Achi\'el: \nhttps://www.google.com/search?q=%s Keyaj taq jaloj richin ri rutikirib\'al molojri\'ïl. Tinuk\' + + Titz\'aqatisäx k\'ak\'a\' rub\'i\' taqoya\'l Najowäx ewan tzij + + Najowäx rub\'i\' okisanel + + Najowäx rub\'i\' ruk\'u\'x k\'amb\'ey Tikanöx chi ch\'ab\'äl @@ -1867,6 +2010,11 @@ Achi\'el: \nhttps://www.google.com/search?q=%s https://www.example.com + + Ri web ochochib\'äl k\'o chi ruk\'wan "https://" or "http://" + + Najowäx okel rub\'i\' ruk\'u\'x k\'amab\'ey + Tokisäx jun chik okisab\'äl @@ -1885,19 +2033,21 @@ Achi\'el: \nhttps://www.google.com/search?q=%s Majun ruwi\' ejaqon + + Richin natz\'aqatisaj jun k\'ak\'a\' ruq\'a\', tayuju\' jun. Tapitz\'a\' ri ruxaq chuqa\' tacha\' tiyuj. ÜTZ, Wetaman Chik Chojmin Okem - - B\'i\'aj + + B\'i\'aj Rub\'i\' ri choj okem - - ÜTZ - - Tiq\'at + + ÜTZ + + Tiq\'at Taq nuk\'ulem @@ -1906,10 +2056,22 @@ Achi\'el: \nhttps://www.google.com/search?q=%s To\'on + + Kerik\' chupun taq ruwi\' + + Kek\'ol chupun taq ruwi\' + ¿La ruyonil nitz\'apïx chi rij jun ik\'? + + Firefox nitikïr yerutz\'apij taq ruxaq man e\'atz\'eton ta pa ri ruk\'isib\'äl ik\'. + + TITZIJ RUYON NICHUPUTÄJ + + Tzijïl ruyon nitz\'apitäj + Firefox Suggest @@ -1933,15 +2095,69 @@ Achi\'el: \nhttps://www.google.com/search?q=%s Titz\'apïx + + + B\'anob\'äl richin yach\'ob\'on + + Tilitäj ch\'aqa\' chik Tetamäx ch\'aqa\' chik To\'on + + Titzij telemetry richin yetaq taq tzij Tib\'e pa taq nuk\'ulem + + + Jikib\'anel taq nik\'oj + + Jikib\'anel taq Nik\'oj + + Tawetamaj ch\'aqa\' chik pa ruwi\' %s. + + + Taq nuk\'ulem + + Tetamäx ch\'aqa\' chik + + %s richin Mozilla + + K\'ak\'a\' rutzijol richin nijikib\'äx + + Tijikib\'äx wakami + + ¡Matyox ruma xatäq rutzijol! + + Xik\'o pa nuwi\' + + Tetamäx ch\'aqa\' chik + + ichinan na\'oj + + Ichinan na\'oj + + Wakami mani + + Beta + + Tik\'ut jub\'a\' + + Kek\'ut ch\'aqa\' chik + + Rajil + + Taqoj + Tich\'utinarisäx + + ch\'utinarisan + + tirik\' + + nimirisan diff --git a/app/src/main/res/values-co/strings.xml b/app/src/main/res/values-co/strings.xml index 812c13d488..51c1bda477 100644 --- a/app/src/main/res/values-co/strings.xml +++ b/app/src/main/res/values-co/strings.xml @@ -76,7 +76,12 @@ private mode in our new Total Private Browsing mode. The first parameter is the name of the app defined in app_name (for example: Firefox Nightly) The second parameter is the clickable link text in felt_privacy_info_card_subtitle_link_text --> - %1$s squassa i vostri canistrelli, cronolugia è dati di siti quandu vo chjudite tutte e vostre finestre private. %2$s + %1$s squassa i vostri canistrelli, cronolugia è dati di siti quandu vo chjudite tutte e vostre finestre private. %2$s + + %1$s squassa i vostri canistrelli, cronolugia è dati di siti quandu vo chjudite tutte e vostre unghjette private. %2$s @@ -206,6 +211,8 @@ Bibliuteca Versione urdinatore + + Apre in un’unghjetta nurmale Aghjunghje à u screnu d’accolta @@ -262,7 +269,7 @@ Numerizà - Mutore di ricerca + Mutore di ricerca Preferenze di u mutore di ricerca @@ -272,7 +279,7 @@ Ùn permette micca - Permette e sugestioni di ricerca in e sessioni private ? + Permette e suggestioni di ricerca in e sessioni private ? %s manderà tuttu ciò chì vò stampittate in a barra di ricerca à u vostru mutore di ricerca predefinitu. @@ -329,10 +336,14 @@ - Impiegate Firefox cum’è navigatore predefinitu + Impiegate Firefox cum’è navigatore predefinitu + + A vostra prutezzione cunta per noi - Firefox fà passà a ghjente nanzu à i prufiti è prutege a vostra vita privata blucchendu l’elementi intersiti di spiunagiu.\n\nSapene di più in a nostra dichjarazione di cunfidenzialità. + Firefox fà passà a ghjente nanzu à i prufiti è prutege a vostra vita privata blucchendu l’elementi intersiti di spiunagiu.\n\nSapene di più in a nostra dichjarazione di cunfidenzialità. + + U nostru navigatore sustinutu da un urganismu senza scopu lucrativu impedisce l’imprese di seguitavvi da manera sicreta nant’à u Web.\n\nSapene di più in a nostra dichjarazione di cunfidenzialità. dichjarazione di cunfidenzialità @@ -342,19 +353,30 @@ Micca subitu - Passate da u telefonu à l’urdinatore purtavule è vice versa + Passate da u telefonu à l’urdinatore purtavule è vice versa + + Prutigitevi grazia à a cifratura quandu vo passate da un apparechju à l’altru - Ricuperate l’unghjette è e parolle d’intesa da i vostri altri apparechji per rivene induve vo l’avete lasciatu. + Ricuperate l’unghjette è e parolle d’intesa da i vostri altri apparechji per rivene induve vo l’avete lasciatu. + + Quandu site cunnessi cù a sincrunizazione attivata, a vostra sicurità hè rinfurzata. Firefox cifra e vostre parolle d’intesa, e vostre indette, è ancu di più. Cunnettesi Micca subitu - E nutificazioni vi aiutanu à fane di più cù Firefox + E nutificazioni vi aiutanu à fane di più cù Firefox + + E nutificazioni vi aiutanu à stà in sicurità cù Firefox + + Mandate l’unghjette trà i vostri apparechji, urganizate i scaricamenti è ricivete cunsiglii per sapè cumu ottene u più bonu di Firefox. - Mandate l’unghjette trà i vostri apparechji, urganizate i scaricamenti è ricivete cunsiglii per sapè cumu ottene u più bonu di Firefox. + Note: The word "Firefox" should NOT be translated --> + Mandate in sicurità l’unghjette trà i vostri apparechji è scuprite d’altre funzioni di cunfidenzialità di Firefox. Attivà e nutificazioni @@ -393,7 +415,9 @@ Selezziunà una trà l’ozzioni. - Urganizà l’accurtatoghji di ricerca + Urganizà l’accurtatoghji di ricerca + + Ghjestione di l’altri mutori di ricerca Mudificà i mutori chì sò videvule in u listinu di ricerca @@ -404,9 +428,17 @@ Ricerca Mutori di ricerca + + Suggestioni di i mutori di ricerca - Barra d’indirizzu - + Barra d’indirizzu + + + Preferenze per a barra d’indirizzu + + Barra d’indirizzu - Firefox suggerisce + + Sapene di più nant’à Firefox suggerisce Appone una nota nant’à Google Play - Riduzzione di e striscie di cannistrelli + Riduzzione di e striscie di cannistrelli + + Blucchime di e striscie di cannistrelli + + Blucchime di e striscie di cannistrelli in navigazione privata - Riduce e striscie di canistrelli + Riduce e striscie di canistrelli - Disattivata + Disattivata - Attivata + Attivata - %1$s prova autumaticamente di righjittà e dumande di canistrelli quandu ci hè striscie di canistrelli. + %1$s prova autumaticamente di righjittà e dumande di canistrelli quandu ci hè striscie di canistrelli. Disattivata per stu situ @@ -462,28 +498,41 @@ Attualmente u situ ùn hè micca accettatu - Attivà a riduzzione di e striscie di cannistrelli per %1$s ? + Attivà a riduzzione di e striscie di cannistrelli per %1$s ? + + Attivà u blucchime di e striscie di cannistrelli per %1$s ? - Disattivà a riduzzione di e striscie di cannistrelli per %1$s ? + Disattivà a riduzzione di e striscie di cannistrelli per %1$s ? + + Disattivà u blucchime di e striscie di cannistrelli per %1$s ? %1$s ùn pò micca righjittà autumaticamente e dumande di canistrelli nant’à stu situ. Pudete mandà una richiesta per ch’ellu sia accettallu in u futuru. - %1$s squasserà i canistrelli di stu situ è attualizerà a pagina. A squassatura di tutti i canistrelli puderia discunnettevi o viutà e sporte di comprera. + %1$s squasserà i canistrelli di stu situ è attualizerà a pagina. A squassatura di tutti i canistrelli puderia discunnettevi o viutà e sporte di comprera. + + Disattivate l’ozzione è %1$s squasserà i canistrelli è ricaricherà stu situ. St’azzioni ponu discunettevi o viutà e vostre sporte di comprera. + + %1$s prova autumaticamente di righjittà tutte e dumande di canistrelli nant’à i siti accettati. - %1$s prova autumaticamente di righjittà tutte e dumande di canistrelli nant’à i siti accettati. + Attivate l’ozzione è %1$s pruverà di righjittà autumaticamente tutte e striscie di canistrelli nant’à stu situ. - Permette à %1$s di righjettà e striscie di canistrelli ? + Permette à %1$s di righjettà e striscie di canistrelli ? - %1$s pò righjittà autumaticamente parechje dumande di striscie di canistrelli. + %1$s pò righjittà autumaticamente parechje dumande di striscie di canistrelli. - Micca subitu + Micca subitu - Viderete menu richieste di cannistrelli + Viderete menu richieste di cannistrelli - Permette + Permette + + + %1$s hà righjittatu i canistrelli per voi + + Menu distrazzioni, menu canistrelli chì vi spiunanu nant’à stu situ. Tentativu autumaticu di cunnessione à i siti impieghendu u protocollu di cifratura HTTPS per aumentà a sicurità. @@ -548,13 +597,13 @@ Affissà i mutori di ricerca - Affissà e sugestioni di ricerca + Affissà e suggestioni di ricerca Affissà a ricerca vucale Affissà durante e sessioni di navigazione privata - Affissà e sugestioni di u preme’papei + Affissà e suggestioni di u preme’papei Ricercà in a cronolugia di navigazione @@ -568,15 +617,15 @@ Compie autumaticamente l’indirizzi - Sugestioni da i finanzieri + Suggestioni da i finanzieri - Sustene %1$s cù sugestioni finanziare occasiunale + Sustene %1$s cù suggestioni finanziare occasiunale - Sugestioni da %1$s + Suggestioni da %1$s - Ottene sugestioni da u web relative à a vostra ricerca + Ottene suggestioni da u web relative à a vostra ricerca Apre i liami in l’appiecazioni @@ -596,6 +645,8 @@ Moduli addiziunali + + Installà un modulu addiziunale da un schedariu Nutificazioni @@ -659,16 +710,10 @@ %s classicu - - Edizione limitata Seguite d’artista - - A nova cullezzione di Voci indipendente. %s A cullezzione Voci indipendente. %s - - A nova cullezzione di Voci indipendente. A cullezzione Voci indipendente. @@ -678,6 +723,14 @@ Scuprite d’altri sfonduli di screnu + + + Novi moduli addiziunali dispunibule + + Scuprite più di 100 estensioni nove chì vi permettenu di persunalizà Firefox. + + Esplurà i moduli addiziunali + Stu modulu addiziunale ùn hè micca accettatu @@ -999,9 +1052,9 @@ Nome di a cullezzione - Rinuminà - - Caccià + Rinuminà + + Caccià Squassà da a cronolugia @@ -1309,7 +1362,7 @@ Per mandà un’unghjetta, cunnittitevi à u vostru contu Firefox nant’à omancu un altru apparechju. - Ahju capitu + Aghju capitu Ùn pò micca sparte st’appiecazione @@ -1423,14 +1476,11 @@ %d unghjette - Cronolugia di navigazione è dati di siti Cronolugia di navigazione %d indirizzi web - - Canistrelli Canistrelli è dati di siti @@ -1486,63 +1536,10 @@ Gruppu squassatu - - Benvenuta in un Internet più bellu - - Un navigatore cuncepitu per a ghjente, micca per i prufiti. - - Ripigliate induve vi site piantati - - Sincrunizate l’unghjette è e parolle d’intesa trà i vostri apparechji per passà d’un screnu à l’altru senza straziu. - - Cunnettesi Sincrunizazione attivata - - Prutezzione attiva di a vita privata - - %1$s impedisce autumaticamente l’imprese di seguitavvi d’una manera sicreta nant’à u Web. - - A funzione di prutezzione tutale contr’à i canistrelli permette d’impedisce l’elementi di spiunagiu d’impiegà i canistrelli per seguitavvi d’un situ à l’altru. - - Classica (predefinita) - - Equilibratu trà a cunfidenzialità è a perfurmenza. E pagine si caricanu nurmalmente. - - Severa - - Bluccheghja più di perseguitatori è cusì e pagine si caricanu piu prestu, ma certi siti puderianu ùn micca funziunà currettamente. - - Sceglie a pusizione di a vostra barra d’attrezzi - - Lasciatela quaghjò o dispiazzatela quassù. - - Gardate u cuntrollu di i vostri dati - - Firefox vi dà u cuntrollu nant’à ciò chì vò spartite in linea è nant’à ciò chì vò spartite cù noi. - - Leghjite a nostra pulitica di cunfidenzialità - - - Pronti à scopre un Internet incredibule ? - - Principià a navigazione - - Sciglite u vostru tema - - - Ecunumizate a vostra batteria è i vostri ochji grazia à u modu scuru. - - Autumaticu - - S’adatta à e preferenze di u vostru apparechju - - Tema scuru - - Tema chjaru - Unghjette mandate ! @@ -1956,7 +1953,7 @@ Mudificà u mutore di ricerca - Aghjunghje + Aghjunghje Arregistrà @@ -1965,17 +1962,17 @@ Squassà - Altru + Altru Nome - Nome + Nome Nome di u mutore di ricerca Indirizzu web di a catena di ricerca - Catena di ricerca à impiegà + Catena di ricerca à impiegà Indirizzu web à impiegà per a ricerca @@ -1985,11 +1982,9 @@ Detaglii di u mutore di ricerca persunalizatu - API per e sugestioni di ricerca (ozzionale) + API per e suggestioni di ricerca (ozzionale) - Indirizzu web di l’API per e sugestioni di ricerca - - Rimpiazzà a richiesta da « %s». Esempiu :\nhttp://suggestqueries.google.com/complete/search?client=firefox&q=%s + Indirizzu web di l’API per e suggestioni di ricerca Rimpiazzà a richiesta da « %s». Esempiu :\nhttps://suggestqueries.google.com/complete/search?client=firefox&q=%s @@ -2124,14 +2119,14 @@ Accurtatoghji - - Nome + + Nome Nome di l’accurtatoghju - - Vai - - Abbandunà + + Vai + + Abbandunà Preferenze @@ -2236,27 +2231,29 @@ Impieghemu a tecnolugia AI di %s da Mozilla per verificà s’è l’avisi nant’à i prudutti sò degni di cunfidenza. Què vi aiuterà à misurà a qualità di l’avisu, micca quella di u pruduttu. - lettera di rangu da A fine à F.]]> + nota alfabetica da A fine à F.]]> - Avisi degni di cunfidenza. Cridemu chì l’avisi venenu sicuramente da clienti veri chì anu lasciatu avisi sinceri è imparziali. + Avisi degni di cunfidenza. Pensemu chì l’avisi venenu sicuramente da clienti veri chì anu lasciatu avisi sinceri è imparziali. Pensemu chì l’avisi sò di cunfidenza. - Cridemu chì ci hè un mischju d’avisi più o menu degni di cunfidenza. + Pensemu chì ci hè un mischju d’avisi più o menu degni di cunfidenza. - Avisi micca degni di cunfidenza. Cridemu chì l’avisi sò sicuramente falsi o ch’elli venenu da persone partigiane. + Avisi micca degni di cunfidenza. Pensemu chì l’avisi sò sicuramente falsi o ch’elli venenu da persone partigiane. Pensemu chì l’avisi ùn sò micca di cunfidenza. - valutazione rettificata si basa solu nant’à l’avisi chì no cridemu degni di cunfidenza.]]> + valutazione rettificata si basa solu nant’à l’avisi chì no pensemu degni di cunfidenza.]]> - messi in lume sò l’avisi %s durante l’ultimi 80 ghjorni chì no cridemu degni di cunfidenza.]]> + messi in lume sò l’avisi %s durante l’ultimi 80 ghjorni chì no pensemu degni di cunfidenza.]]> Sapene di più nant’à %s. - cumu %s da Mozilla determineghja a qualità di l’avisi + cumu %s da Mozilla determineghja a qualità di l’avisi + + cumu %s determineghja a qualità di l’avisi Preferenze @@ -2275,13 +2272,11 @@ Publicità da %s - U verificadore d’avisu funziuneghja grazia à %s. - U verificadore d’avisu funziuneghja grazia à %s %s da Mozilla - Infurmazione nova à cuntrollà + Infurmazioni nove à cuntrollà Cuntrollà subitu @@ -2302,11 +2297,11 @@ Cuntrollà a qualità di l’avisi - St’operazione puderià piglià 60 seconde + St’operazione pò piglià 60 seconde Vi ringraziemu di u vostru signalamentu ! - Duveriamu avè l’infurmazioni nant’à l’avisi di stu pruduttu nanzu 24 ore. Ci vole à verificà dopu. + Duveriamu avè l’infurmazioni nant’à l’avisi di stu pruduttu da quì à 24 ore. Ci vole à verificà dopu. Ùn pudemu micca cuntrollà st’avisi @@ -2314,17 +2309,17 @@ Infurmazione dispunibule frà pocu - Duveriamu avè l’infurmazioni nant’à l’avisi di stu pruduttu nanzu 24 ore. Ci vole à verificà dopu. + Duveriamu avè l’infurmazioni nant’à l’avisi di stu pruduttu da quì à 24 ore. Ci vole à verificà dopu. L’analisa hè l’ultima - Ahju capitu + Aghju capitu Alcuna infurmazione dispunibule fine à quì Travagliemu per currege stu penseru. Ci vole à verificà ulteriurmente. - Alcuna cunnessione di reta + Nisuna cunnessione di reta Verificate a vostra cunnessione di reta è pruvate di ricaricà a pagina. @@ -2337,16 +2332,24 @@ Pruvate a nostra guida di cunfidenza nant’à l’avisi di prudutti Fighjate quantu sò degni di cunfidenza l‘avisi di prudutti nant’à %1$s prima di cumprà. U verificadore d’avisu, una funzione esperimentale da %2$s, hè integratu direttamente in u navigatore. Funziuneghja ancu nant’à %3$s è %4$s. + + Fighjate quantu sò degni di cunfidenza l‘avisi di prudutti nant’à %1$s prima di cumprà. U verificadore d’avisu, una funzione esperimentale da %2$s, hè integratu direttamente in u navigatore. Impieghendu a tecnolugia di %1$s da Mozilla, vi aiutemu à rispinghje l’avisi partigiani è falsi. U nostru mudellu d’intelligenza artificiale hè amendatu cuntinuatamente per prutegevi quandu vo cumprate. %2$s Sapene di più - A selezzione di « Sì, pruvallu » vole dì chì vo site d.accunsenti cù %2$s è %3$s di %1$s da Mozilla. + A selezzione di « Sì, pruvallu » vole dì chì vo site d.accunsenti cù %2$s è %3$s di %1$s da Mozilla. + + A selezzione di « Sì, pruvallu » vole dì chì vo accettate quell’elementi di %1$s : - pulitica di cunfidenzialità + pulitica di cunfidenzialità + + Pulitica di cunfidenzialità + + cundizioni d’utilizazione - cundizioni d’utilizazione + Cundizioni d’utilizazione Sì, pruvallu @@ -2387,12 +2390,18 @@ riduce + + riduttu allargà + + allargatu apre u liame per sapene di più nant’à sta cullezzione leghje l’articulu apre u liame per sapene di più + + %s, Titulu diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 2fab3dbeb3..7bb530bb55 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -525,6 +525,11 @@ Povolit + + %1$s za vás odmítl cookies + + Méně rozptylování, méně souborů cookie, které vás na tomto webu sledují. + Pro zvýšení zabezpečení se automaticky pokusí připojit k webům pomocí šifrovacího protokolu HTTPS. @@ -635,6 +640,8 @@ Doplňky + + Nainstalovat doplněk ze souboru Oznámení @@ -2261,8 +2268,6 @@ Reklama od %s - Kontrola recenzí používá technologii %s. - Kontrola recenzí používá technologii %s %s od Mozilly @@ -2322,6 +2327,8 @@ Vyzkoušejte našeho důvěryhodného průvodce recenzemi produktů Před nákupem se podívejte, jak spolehlivé jsou recenze produktů na %1$s. Kontrola recenzí, experimentální funkce od %2$s, je integrována přímo do prohlížeče. Funguje také na %3$s a %4$s. + + Před nákupem se podívejte, jak spolehlivé jsou recenze produktů na %1$s. Kontrola recenzí je experimentální funkce od %2$s a je zabudována v prohlížeči. Pomocí funkce %1$s by Mozilla vám pomůžeme vyhnout se neobjektivním a neautentickým recenzím. Náš model umělé inteligence se neustále zdokonaluje, aby vás při nakupování chránil. %2$s @@ -2388,4 +2395,6 @@ přečíst článek otevřít odkaz pro další informace + + %s, nadpis diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index fe38db2f52..e774b3cecd 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -462,6 +462,8 @@ Cyfyngu Baneri Cwcis Rhwystrydd Baneri Cwcis + + Rhwystro Baner Cwci wrth bori’n breifat Lleihau baneri cwcis @@ -491,16 +493,18 @@ Troi Llai o Faneri Cwcis ymlaen ar %1$s? - Trowch yr Atalydd Baner Cwci ymlaen ar gyfer %1$s? + Trowch y Rhwystrydd Baner Cwci ymlaen ar gyfer %1$s? Diffodd Llai o Faneri Cwcis ar %1$s? - Diffodd yr Atalydd Baner Cwcis am %1$s? + Diffoddwch y Rhwystrydd Baner Cwcis ar gyfer %1$s? Nid yw %1$s yn gallu gwrthod ceisiadau cwcis ar y wefan hon yn awtomatig. Gallwch anfon cais i gefnogi’r wefan hon yn y dyfodol. Bydd %1$s yn clirio cwcis y wefan hon ac yn adnewyddu’r dudalen. Gall clirio pob cwci eich allgofnodi neu wagio eich certiau siopa. + + Diffoddwch a bydd %1$s yn clirio cwcis ac yn ail-lwytho’r wefan hon. Gall hyn eich allgofnodi neu wagio cartiau siopa. Mae %1$s yn ceisio gwrthod yn awtomatig pob cais cwci ar wefannau sy’n cael eu cefnogi. @@ -518,6 +522,11 @@ Caniatáu + + Mae %1$s newydd wrthod cwcis i chi + + Llai o darfu, llai o gwcis yn eich tracio ar y wefan hon. + Yn ceisio cysylltu’n awtomatig â gwefannau gan ddefnyddio’r protocol amgryptio HTTPS am fwy o ddiogelwch. @@ -628,6 +637,8 @@ Ychwanegion + + Gosod ychwanegyn o ffeil Hysbysiadau @@ -2234,8 +2245,6 @@ Hysbyseb gan %s - Mae’r gwirydd adolygiadau yn cael ei bweru gan %s. - Mae’r gwirydd adolygiadau yn cael ei bweru gan %s %s gan Mozilla @@ -2295,6 +2304,8 @@ Rhowch gynnig ar ein canllaw dibynadwy i adolygiadau cynnyrch Gweld pa mor ddibynadwy yw adolygiadau cynnyrch ar %1$s cyn i chi brynu. Mae gwirydd adolygiadau, nodwedd arbrofol gan %2$s, wedi’i gynnwys yn y porwr. Mae’n gweithio ar %3$s a %4$s hefyd. + + Gweld pa mor ddibynadwy yw adolygiadau cynnyrch ar %1$s cyn i chi brynu. Mae\'r Gwirydd Adolygiadau, nodwedd arbrofol gan %2$s, wedi\'i gynnwys yn y porwr. Gan ddefnyddio pŵer %1$s gan Mozilla, rydym yn eich helpu i osgoi adolygiadau rhagfarnllyd ac annilys. Mae ein model AI bob amser yn gwella i’ch diogelu wrth i chi siopa. %2$s @@ -2361,4 +2372,6 @@ darllen yr erthygl agor dolen i wybod rhagor + + %s, Pennyn diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index d3537dc30c..a3318a8f99 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -522,6 +522,11 @@ Tillad + + %1$s har lige afvist cookies for dig + + Færre distraktioner og færre cookies, der sporer dig på dette websted. + Forsøger automatisk at oprette forbindelse til websteder ved hjælp af krypteringsprotokollen HTTPS for øget sikkerhed. @@ -630,6 +635,8 @@ Tilføjelser + + Installer tilføjelse fra fil Meddelelser @@ -2229,8 +2236,6 @@ Reklame fra %s - Verificering af anmeldelser er leveret af %s. - Verificering af anmeldelser er leveret af %s %s fra Mozilla @@ -2291,6 +2296,8 @@ Se hvor pålidelige produktanmeldelser på %1$s er, inden du køber. Verificering af anmeldelser, en eksperimental funktion fra %2$s, er indbygget i browseren. Den virker også på %3$s og %4$s. + + Se hvor pålidelige produktanmeldelser på %1$s er, inden du køber. Verificering af anmeldelser, en eksperimental funktion fra %2$s, er indbygget i browseren. Ved hjælp af %1$s fra Mozilla gør vi det nemmere for dig undgå partiske og uægte anmeldelser. Vores kunstig intelligens-model forbedres altid for at beskytte dig, mens du handler. %2$s @@ -2357,4 +2364,6 @@ læse artiklen åbne link for at læse mere + + %s, overskrift diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6b62151960..5eebd8a8f5 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -355,7 +355,7 @@ Wechseln Sie vom Handy zum Laptop und zurück - Verschlüsseln Sie ihre Daten, wenn Sie geräteübergreifend arbeiten + Verschlüsseln Sie Ihre Daten, wenn Sie geräteübergreifend arbeiten Holen Sie sich die Tabs und Passwörter von Ihren anderen Geräten, um dort weiterzumachen, wo Sie aufgehört haben. Erlauben + + %1$s hat Cookies für Sie abgelehnt + + Weniger Ablenkungen, weniger Cookies, die Sie auf dieser Website verfolgen. + Automatisch versuchen, eine Verbindung zu Websites herzustellen, die das HTTPS-Verschlüsselungsprotokoll verwenden, um die Sicherheit zu erhöhen. @@ -637,6 +642,8 @@ Add-ons + + Add-on aus Datei installieren Benachrichtigungen @@ -2273,8 +2280,6 @@ Anzeige von %s - Rezensionsprüfer wird bereitgestellt von %s. - Rezensionsprüfer wird bereitgestellt von %s %s von Mozilla @@ -2334,6 +2339,8 @@ Lesen Sie unseren vertrauenswürdigen Leitfaden zur Produktbewertung Sehen Sie sich vor dem Kauf an, wie zuverlässig Produktbewertungen auf %1$s sind. Der Rezensionsprüfer, eine experimentelle Funktion von %2$s, ist direkt in den Browser integriert. Es funktioniert auch auf %3$s und %4$s. + + Sehen Sie sich vor dem Kauf an, wie zuverlässig Produktbewertungen auf %1$s sind. Der Rezensionsprüfer, eine experimentelle Funktion von %2$s, ist direkt in den Browser integriert. Wir nutzen das Potenzial von %1$s von Mozilla, um Ihnen dabei zu helfen, voreingenommene und nicht authentische Bewertungen zu vermeiden. Unser KI-Modell wird ständig verbessert, um Sie beim Einkaufen zu schützen. %2$s @@ -2400,4 +2407,6 @@ den Artikel zu lesen Link öffnen, um mehr zu erfahren + + %s, Überschrift diff --git a/app/src/main/res/values-dsb/strings.xml b/app/src/main/res/values-dsb/strings.xml index 1c27572580..4f44f48b69 100644 --- a/app/src/main/res/values-dsb/strings.xml +++ b/app/src/main/res/values-dsb/strings.xml @@ -520,6 +520,11 @@ Dowóliś + + %1$s jo rowno wótpokazał cookieje za was + + Mjenjej wótchylenjow, mjenjej cookiejow, kótarež was na sedle pśeslěduju. + Wopytujo z pomocu koděrowańskego protokola HTTPS za pówušonu wěstotu awtomatiski ze sedłami zwězaś. @@ -629,6 +634,8 @@ Dodanki + + Dodank z dataje instalěrowaś Powěźeńki @@ -2241,8 +2248,6 @@ To buźo jano pomagaś, kwalitu pógódnośenjow pósuźiś, nic kwalitu produkt Wabjenje wót %s - Kontrola pógódnośenjow se wót %s spěchujo. - Kontrola pógódnośenjow se wót %s spěchujo %s wót Mozilla @@ -2303,6 +2308,8 @@ To buźo jano pomagaś, kwalitu pógódnośenjow pósuźiś, nic kwalitu produkt Cytajśo naš dowěry gódny pśewodnik wó pógódnośenjach produktow Glědajśo, kak spušćobne pógódnośenja produktow na %1$s su, nježli až produkty kupujośo. Kontrola pógódnośenjow, eksperimentalna funkcija z %2$s, jo direktnje do wobglědowaka zatwarjona. Funkcioněrujo teke na %3$s a %4$s. + + Glědajśo, kak spušćobne pógódnośenja produktow na %1$s su, nježli až produkty kupujośo. Kontrola pógódnośenjow, eksperimentalna funkcija z %2$s, jo direktnje do wobglědowaka zatwarjona. Z pomocu potenciala %1$s wót Mozilla, pomagamy wam, se njeawtentiskich pógódnośenjow a pógódnośenjow połnych pśedsudkow wobinuś. Naš model KI se stawnje pólěpšujo, aby was šćitał, gaž nakupujośo. %2$s @@ -2369,4 +2376,6 @@ To buźo jano pomagaś, kwalitu pógódnośenjow pósuźiś, nic kwalitu produkt nastawk cytaś wótkaz wócyniś, aby wy wěcej zgónił + + %s, nadpismo diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index f26bc4facf..7ae8ef7f23 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -530,6 +530,11 @@ Αποδοχή + + Το %1$s μόλις αρνήθηκε τα cookie για εσάς + + Λιγότεροι περισπασμοί, λιγότερα cookie που σας παρακολουθούν σε αυτόν τον ιστότοπο. + Προσπαθεί αυτόματα να συνδεθεί σε ιστοτόπους με το πρωτόκολλο κρυπτογράφησης HTTPS για αυξημένη ασφάλεια. @@ -639,6 +644,8 @@ Πρόσθετα + + Εγκατάσταση προσθέτου από αρχείο Ειδοποιήσεις @@ -2263,8 +2270,6 @@ Διαφήμιση από το %s - Ο έλεγχος κριτικών παρέχεται από το %s. - Ο έλεγχος κριτικών παρέχεται από το %s %s από τη Mozilla @@ -2324,6 +2329,8 @@ Δοκιμάστε τον αξιόπιστο οδηγό μας για κριτικές προϊόντων Δείτε πόσο αξιόπιστες είναι οι κριτικές των προϊόντων στο %1$s πριν αγοράσετε. Ο έλεγχος κριτικών, μια πειραματική δυνατότητα από τo %2$s, ενσωματώνεται στο πρόγραμμα περιήγησης. Λειτουργεί επίσης στο %3$s και στο %4$s. + + Δείτε πόσο αξιόπιστες είναι οι κριτικές των προϊόντων στο %1$s πριν αγοράσετε. Ο έλεγχος κριτικών, μια πειραματική δυνατότητα από τo %2$s, ενσωματώνεται στο πρόγραμμα περιήγησης. Με την υποστήριξη του %1$s της Mozilla, σας βοηθάμε να αποφύγετε τις μεροληπτικές και ψευδείς κριτικές. Το μοντέλο AI μας βελτιώνεται συνεχώς για να σας προστατεύει όσο κάνετε τις αγορές σας. %2$s @@ -2390,4 +2397,6 @@ ανάγνωση του άρθρου άνοιγμα συνδέσμου για περισσότερες πληροφορίες + + %s, Επικεφαλίδα diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index 90a6d35171..6e1ceea218 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -519,6 +519,11 @@ Allow + + %1$s just refused cookies for you + + Less distractions, less cookies tracking you on this site. + Automatically attempts to connect to sites using HTTPS encryption protocol for increased security. @@ -629,6 +634,8 @@ Add-ons + + Install add-on from file Notifications @@ -2229,8 +2236,6 @@ Ad by %s - Review checker is powered by %s. - Review checker is powered by %s %s by Mozilla @@ -2290,6 +2295,8 @@ Try our trusted guide to product reviews See how reliable product reviews are on %1$s before you buy. Review checker, an experimental feature from %2$s, is built right into the browser. It works on %3$s and %4$s, too. + + See how reliable product reviews are on %1$s before you buy. Review Checker, an experimental feature from %2$s, is built right into the browser. Using the power of %1$s by Mozilla, we help you avoid biased and inauthentic reviews. Our AI model is always improving to protect you as you shop. %2$s @@ -2356,4 +2363,6 @@ read the article open link to learn more + + %s, Heading diff --git a/app/src/main/res/values-es-rAR/strings.xml b/app/src/main/res/values-es-rAR/strings.xml index 8efda3678d..7a831c2c3e 100644 --- a/app/src/main/res/values-es-rAR/strings.xml +++ b/app/src/main/res/values-es-rAR/strings.xml @@ -528,6 +528,11 @@ Permitir + + %1$s acaba de rechazar las cookies + + Menos distracciones, menos cookies que te rastrean en este sitio. + Intenta conectarse automáticamente a sitios usando el protocolo de cifrado HTTPS para mayor seguridad. @@ -638,6 +643,8 @@ Complementos + + Instalar complemento desde archivo Notificaciones @@ -1472,7 +1479,7 @@ Eliminar datos de navegación - Abrir pestañas + Pestañas abiertas %d pestañas @@ -2266,8 +2273,6 @@ Publicidad de %s - El verificador de revisiones funciona con %s. - El verificador de revisiones funciona con %s %s de Mozilla @@ -2327,14 +2332,16 @@ Probá nuestra guía confiable de revisiones de productos Fijate que tan confiables son las revisiones de productos en %1$s antes de comprar. El verificador de revisiones, una funcionalidad experimental de %2$s, está integrado en el navegador. Funciona también en %3$s y %4$s. + + Fijate que tan confiables son las revisiones de productos en %1$s antes de comprar. El verificador de revisiones, una funcionalidad experimental de %2$s, está integrado en el navegador. Usando el poder de %1$s de Mozilla, te ayudamos a evitar revisiones sesgadas y no auténticas. Nuestro modelo de IA siempre está mejorando para protegerte mientras comprás. %2$s Conocer más - Al seleccionar “Si, probalo” Estás de acuerdo con %1$s de Mozilla %2$s y %3$s. + Al seleccionar “Si, probarlo” Estás de acuerdo con %1$s de Mozilla %2$s y %3$s. - Al seleccionar “Sí, probalo” aceptás lo siguiente de%1$s: + Al seleccionar “Sí, probarlo” aceptás lo siguiente de %1$s: política de privacidad @@ -2344,7 +2351,7 @@ Términos de uso - Sí, probalo + Sí, probarlo No ahora @@ -2393,4 +2400,6 @@ leer el artículo abrir enlace para conocer más + + %s, encabezado diff --git a/app/src/main/res/values-es-rCL/strings.xml b/app/src/main/res/values-es-rCL/strings.xml index 317519dc2e..fec0726c1d 100644 --- a/app/src/main/res/values-es-rCL/strings.xml +++ b/app/src/main/res/values-es-rCL/strings.xml @@ -520,6 +520,11 @@ Permitir + + %1$s acaba de rechazar las cookies por ti + + Menos distracciones, menos cookies que te siguen en este sitio. + Intenta conectarse automáticamente a sitios utilizando el protocolo de cifrado HTTPS para mayor seguridad. @@ -630,6 +635,8 @@ Complementos + + Instalar complemento desde archivo Notificaciones @@ -2235,8 +2242,6 @@ Anuncio de %s - El verificador de reseñas funciona con %s. - El verificador de reseñas funciona con %s %s de Mozilla @@ -2297,6 +2302,8 @@ Prueba nuestra guía confiable de reseñas de productos Mira cuán confiables son las reseñas de productos en %1$s antes de comprar. El verificador de reseñas, una función experimental de %2$s, está integrado directamente en el navegador. También funciona en %3$s y %4$s. + + Mira cuán confiables son las reseñas de productos en %1$s antes de comprar. El verificador de reseñas, una función experimental de %2$s, está integrado directamente en el navegador. Usando el poder de %1$s de Mozilla, te ayudamos a evitar reseñas sesgadas y no auténticas. Nuestro modelo de IA siempre está mejorando para protegerte mientras compras. %2$s @@ -2363,4 +2370,6 @@ leer el artículo abrir enlace para aprender más + + %s, Cabecera diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 5a3438e1da..ad76654c89 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -528,6 +528,11 @@ Permitir + + %1$s acaba de rechazar las cookies por ti + + Menos distracciones, menos cookies que te rastrean en este sitio. + Intenta conectarse automáticamente a sitios utilizando el protocolo de cifrado HTTPS para mayor seguridad. @@ -638,6 +643,8 @@ Complementos + + Instalar complemento desde archivo Notificaciones @@ -2276,8 +2283,6 @@ Anuncio de %s - El verificador de reseñas funciona gracias a %s. - El verificador de reseñas funciona gracias a %s %s de Mozilla @@ -2337,6 +2342,8 @@ Prueba nuestra fiable guía de reseñas de productos Comprueba lo fiables que son las reseñas de productos en %1$s antes de comprar. El verificador de reseñas, una función experimental de %2$s, está integrado directamente en el navegador. También funciona en %3$s y %4$s. + + Comprueba lo fiables que son las reseñas de productos en %1$s antes de comprar. El verificador de reseñas, una función experimental de %2$s, está integrado directamente en el navegador. Utilizando la tecnología de %1$s de Mozilla, te ayudamos a evitar reseñas sesgadas y no auténticas. Nuestro modelo de IA siempre mejora para protegerte mientras compras. %2$s @@ -2403,4 +2410,6 @@ leer el artículo abrir enlace para saber más + + %s, Cabecera diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 6e5f61582c..ad76654c89 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -209,6 +209,8 @@ Biblioteca Sitio de escritorio + + Abrir en una pestaña normal Agregar a la pantalla de Inicio @@ -267,7 +269,7 @@ Escanear - Buscador + Buscador Ajustes del buscador @@ -465,16 +467,20 @@ Modo solo HTTPS - Reducción de avisos de cookies + Reducción de avisos de cookies + + Bloqueador de avisos de cookies + + Bloqueador de avisos de cookies en navegación privada - Reducir los avisos de cookies + Reducir los avisos de cookies - Desactivado + Desactivado - Activado + Activado - %1$s intenta rechazar automáticamente las solicitudes de cookies en los avisos de cookies. + %1$s intenta rechazar automáticamente las solicitudes de cookies en los avisos de cookies. Desactivada para este sitio @@ -492,27 +498,40 @@ Sitio actualmente no compatible - ¿Activar la reducción de aviso de cookies para %1$s? + ¿Activar la reducción de aviso de cookies para %1$s? + + ¿Activar el bloqueo de aviso de cookies para %1$s? + + ¿Desactivar la reducción de aviso de cookies para %1$s? - ¿Desactivar la reducción de aviso de cookies para %1$s? + ¿Desactivar el bloqueo de aviso de cookies para %1$s? %1$s no puede rechazar automáticamente los avisos de cookies en este sitio. Puede enviar una solicitud para admitir este sitio en el futuro. - %1$s borrará las cookies de este sitio y recargará la página. Borrar todas las cookies puede cerrar tu sesión o vaciar los carritos de compras. + %1$s borrará las cookies de este sitio y recargará la página. Borrar todas las cookies puede cerrar tu sesión o vaciar los carritos de compras. + + Tras desactivarlo, %1$s borrará las cookies y recargará la página. Esto puede desconectarte del sitio o vaciar tu carrito de compra. + + %1$s intenta rechazar automáticamente las solicitudes de cookies en sitios compatibles. - %1$s intenta rechazar automáticamente las solicitudes de cookies en sitios compatibles. + Al activarlo %1$s intentará rechazar automáticamente los avisos de cookies en este sitio. - ¿Permitir que %1$s rechace los avisos de cookies? + ¿Permitir que %1$s rechace los avisos de cookies? - %1$s puede rechazar automáticamente muchas solicitudes de cookies. + %1$s puede rechazar automáticamente muchas solicitudes de cookies. - Ahora no + Ahora no - Verás menos solicitudes de cookies + Verás menos solicitudes de cookies - Permitir + Permitir + + + %1$s acaba de rechazar las cookies por ti + + Menos distracciones, menos cookies que te rastrean en este sitio. Intenta conectarse automáticamente a sitios utilizando el protocolo de cifrado HTTPS para mayor seguridad. @@ -624,6 +643,8 @@ Complementos + + Instalar complemento desde archivo Notificaciones @@ -2242,7 +2263,9 @@ Saber más sobre %s. - cómo %s de Mozilla determina la calidad de las reseñas + cómo %s de Mozilla determina la calidad de las reseñas + + cómo determina %s la calidad de las reseñas Ajustes @@ -2260,8 +2283,6 @@ Anuncio de %s - El verificador de reseñas funciona gracias a %s. - El verificador de reseñas funciona gracias a %s %s de Mozilla @@ -2321,16 +2342,24 @@ Prueba nuestra fiable guía de reseñas de productos Comprueba lo fiables que son las reseñas de productos en %1$s antes de comprar. El verificador de reseñas, una función experimental de %2$s, está integrado directamente en el navegador. También funciona en %3$s y %4$s. + + Comprueba lo fiables que son las reseñas de productos en %1$s antes de comprar. El verificador de reseñas, una función experimental de %2$s, está integrado directamente en el navegador. Utilizando la tecnología de %1$s de Mozilla, te ayudamos a evitar reseñas sesgadas y no auténticas. Nuestro modelo de IA siempre mejora para protegerte mientras compras. %2$s Saber más - Al seleccionar “Sí, probarlo”, aceptas %1$s de %2$s y %3$s de Mozilla. + Al seleccionar “Sí, probarlo”, aceptas %1$s de %2$s y %3$s de Mozilla. + + Al seleccionar “Sí, probarlo”, aceptas lo siguiente de %1$s: - política de privacidad + política de privacidad + + Política de privacidad + + términos de uso - términos de uso + Términos de uso Si, probarlo @@ -2381,4 +2410,6 @@ leer el artículo abrir enlace para saber más + + %s, Cabecera diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 861263513c..9f43c9012a 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -206,6 +206,8 @@ Biltegia Mahaigainerako gunea + + Ireki fitxa arruntean Gehitu hasierako pantailan @@ -265,7 +267,7 @@ Eskaneatu - Bilaketa-motorra + Bilaketa-motorra Bilaketa-motorren ezarpenak @@ -465,17 +467,21 @@ zure pasahitzak, laster-markak eta gehiago zifratzen du. HTTPS-Only modua - Cookie iragarki-banden murrizpena + Cookie iragarki-banden murrizpena + + Cookie iragarki-banden blokeatzailea + + Cookie iragarki-banden blokeatzailea nabigatze pribatuan - Murriztu cookie iragarki-bandak + Murriztu cookie iragarki-bandak - Desaktibatuta + Desaktibatuta - Aktibatuta + Aktibatuta - Cookie iragarki-bandetako eskaerak automatikoki ukatzen saiatzen da %1$s. + Cookie iragarki-bandetako eskaerak automatikoki ukatzen saiatzen da %1$s. Desaktibatuta gune honetarako @@ -493,27 +499,40 @@ zure pasahitzak, laster-markak eta gehiago zifratzen du. Une honetan gune honetarako euskarririk ez - Aktibatu cookie iragarki-banden murrizpena %1$s gunerako? + Aktibatu cookie iragarki-banden murrizpena %1$s gunerako? + + Aktibatu cookie iragarki-banden blokeatzailea %1$s gunerako? + + Desaktibatu cookie iragarki-banden murrizpena %1$s gunerako? - Desaktibatu cookie iragarki-banden murrizpena %1$s gunerako? + Desaktibatu cookie iragarki-banden blokeatzailea %1$s gunerako? %1$s(e)k ezin ditu cookie-eskaerak automatikoki baztertu gune honetan. Etorkizunean gune honetarako euskarria gehitzeko eskaera bidal dezakezu. - %1$s(e)k gune honetako cookieak garbitu eta orria berrituko du. Cookie guztiak garbitzean, saioak amaitu edo erosketa-orgak hustu litezke. + %1$s(e)k gune honetako cookieak garbitu eta orria berrituko du. Cookie guztiak garbitzean, saioak amaitu edo erosketa-orgak hustu litezke. + + Desaktibatu eta %1$s(e)k gune honetako cookieak garbitu eta orria berrituko du. Saioa amaitu edo erosketa-orgak hustu litezke. + + Cookie eskaerak automatikoki ukatzen saiatzen da %1$s. - Cookie eskaerak automatikoki ukatzen saiatzen da %1$s. + Aktibatu eta %1$s gune honetako cookie iragarki-bandak automatikoki ukatzen saiatuko da. - Baimendu %1$s(r)i cookie iragarki-bandak ukatzen? + Baimendu %1$s(r)i cookie iragarki-bandak ukatzen? - %1$s(e)k automatikoki uka ditzake cookie iragarki-bandetako eskaerak. + %1$s(e)k automatikoki uka ditzake cookie iragarki-bandetako eskaerak. - Une honetan ez + Une honetan ez - Cookie eskaera gutxiago ikusiko dituzu + Cookie eskaera gutxiago ikusiko dituzu - Baimendu + Baimendu + + + %1$s(e)k cookieak ukatu ditu zure partez + + Distrazio gutxiago, cookie gutxiago zure jarraipena egiten gune honetan. Automatikoki saiatzen da guneetara konektatzen HTTPS zifratze-protokoloa erabiliz, segurtasun gehiago lortzeko. @@ -625,6 +644,8 @@ zure pasahitzak, laster-markak eta gehiago zifratzen du. Gehigarriak + + Instalatu gehigarria fitxategitik Jakinarazpenak @@ -2219,7 +2240,9 @@ zure pasahitzak, laster-markak eta gehiago zifratzen du. Argibide gehiago %s(r)i buruz. - Mozillaren %s(e)k nola antzematen duen balorazioen kalitatea + Mozillaren %s(e)k nola antzematen duen balorazioen kalitatea + + Nola antzematen duen %s(e)k balorazioen kalitatea Ezarpenak @@ -2237,8 +2260,6 @@ zure pasahitzak, laster-markak eta gehiago zifratzen du. %s(r)en iragarkia - Balorazioen egiaztatzailea %s(e)k hornituta. - Balorazioen egiaztatzailea %s(e)k hornituta Mozillaren %s @@ -2299,16 +2320,24 @@ zure pasahitzak, laster-markak eta gehiago zifratzen du. Probatu produktuen balorazioetarako gure fidatzeko gida Erosi aurretik, ikusi %1$s(e)ko produktuen balorazioak zenbateraino diren fidagarriak. Balorazioen egiaztatzailea, %2$s(e)n eginbide esperimentala, nabigatzailean integratuta dago. %3$s eta %4$s(e)n ere badabil. + + Erosi aurretik, ikusi %1$s(e)ko produktuen balorazioak zenbateraino diren fidagarriak. Balorazioen egiaztatzailea %2$s(e)n eginbide esperimentala da eta nabigatzailean integratuta dago. Mozillaren %1$s(r)en teknologia erabiliz, balorazio aurreiritzidun eta egiazkoak ez direnak saihesten laguntzen dizugu. Gure Adimen Artifizialeko modeloa uneoro ari da hobetzen zure erosketak babesteko. %2$s Argibide gehiago - "Bai, probatu" aukeratuta, Mozillaren %1$s(r)en %2$s eta %3$s onartzen dituzu. + "Bai, probatu" aukeratuta, Mozillaren %1$s(r)en %2$s eta %3$s onartzen dituzu. + + "Bai, probatu" hautatuz gero, ondorengoa onartzen duzu %1$s(e)tik: - pribatutasun-politika + pribatutasun-politika + + Pribatutasun-politika + + erabilera-baldintzak - erabilera-baldintzak + Erabilera-baldintzak Bai, probatu @@ -2359,4 +2388,6 @@ zure pasahitzak, laster-markak eta gehiago zifratzen du. irakurri artikulua ireki lotura argibide gehiagorako + + %s, Goiburua diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index b8958b7a5c..521476598b 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -528,6 +528,11 @@ Salli + + %1$s kieltäytyi juuri evästeistä + + Vähemmän häiriötekijöitä, vähemmän sinua tällä sivustolla seuraavia evästeitä. + Yrittää muodostaa automaattisesti yhteyden sivustoihin käyttämällä salattua HTTPS-protokollaa turvallisuuden parantamiseksi. @@ -638,6 +643,8 @@ Lisäosat + + Asenna lisäosa tiedostosta Ilmoitukset @@ -2255,8 +2262,6 @@ Mainostaja %s - Arvostelun tarkistuksen tarjoaa %s. - Arvostelujen tarkistuksen tarjoaa %s %s Mozillalta @@ -2308,6 +2313,8 @@ Tarkista verkkoyhteytesi ja yritä sitten ladata sivu uudelleen. Näistä arvosteluista ei ole vielä tietoa + + Tarkista arvostelun laatu, jos haluat tietää, ovatko tämän tuotteen arvostelut luotettavia. Se kestää vain noin 60 sekuntia. Tarkista arvostelun laatu @@ -2360,4 +2367,4 @@ lukeaksesi artikkelin avataksesi linkin ja lukeaksesi lisää - + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0dc7ff39ef..818450a5d5 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -510,8 +510,12 @@ %1$s effacera les cookies de ce site et actualisera la page. La suppression de tous les cookies peut vous déconnecter ou vider les paniers d’achats. + + Désactivez-le et %1$s effacera les cookies puis rechargera le site. Ces actions peuvent vous déconnecter ou vider votre panier. %1$s peut essayer de refuser automatiquement les demandes de dépôt de cookies sur les sites compatibles. + + Activez-le et %1$s tentera de refuser automatiquement les bannières de cookies sur ce site. Autoriser %1$s à refuser les bannières de cookies ? @@ -524,6 +528,11 @@ Autoriser + + %1$s a refusé les cookies pour vous + + Moins de distractions, moins de cookies qui vous pistent sur ce site. + Essayer de se connecter automatiquement aux sites en utilisant le protocole de chiffrement HTTPS pour une sécurité accrue. @@ -634,6 +643,8 @@ Modules complémentaires + + Installer un module depuis un fichier Notifications @@ -2267,8 +2278,6 @@ Publicité de %s - Le vérificateur d’avis fonctionne grâce à %s. - Le vérificateur d’avis fonctionne grâce à %s %s par Mozilla @@ -2329,6 +2338,8 @@ Essayez notre guide de confiance pour les avis de produits Regardez quelle fiabilité accorder aux avis de produits sur %1$s avant d’acheter. Le vérificateur d’avis, une fonctionnalité expérimentale de %2$s, est intégré au navigateur. Il fonctionne aussi pour %3$s et %4$s. + + Regardez quelle fiabilité accorder aux avis de produits sur %1$s avant d’acheter. Le vérificateur d’avis, une fonctionnalité expérimentale de %2$s, est intégré au navigateur. En utilisant la puissance de %1$s par Mozilla, nous vous aidons à éviter les avis partiaux et mensongers. Notre modèle d’intelligence artificielle s’améliore en permanence pour vous protéger pendant vos achats. %2$s @@ -2396,4 +2407,6 @@ lire l’article ouvrir le lien pour en savoir plus + + %s, titre diff --git a/app/src/main/res/values-fur/strings.xml b/app/src/main/res/values-fur/strings.xml index 698e722547..257a0767a1 100644 --- a/app/src/main/res/values-fur/strings.xml +++ b/app/src/main/res/values-fur/strings.xml @@ -520,6 +520,11 @@ Permet + + %1$s al à a pene refudât i cookies par te + + Mancul distrazions, mancul cookies che ti stan daûr su chest sît. + Al cîr di conetisi in automatic ai sîts doprant il protocol di cifradure HTTPS par vê une sigurece miorade. @@ -633,6 +638,8 @@ Components adizionâi + + Instale component adizionâl di un file Notifichis @@ -2233,8 +2240,6 @@ Anunzi di %s - La verifiche recensions e à tecnologjie di %s. - Verifiche recensions cun tecnologjie %s %s di Mozilla @@ -2301,6 +2306,8 @@ Scuvierç trop che a son afidabilis lis recensions dai prodots su %1$s prime di comprâju. Verifiche recensions, une funzion sperimentâl di %2$s, e je integrade propite tal navigadôr. E funzione ancje su %3$s e %4$s. + + Scuvierç trop che a son afidabilis lis recensions dai prodots su %1$s prime di comprâju. Verifiche recensions, une funzion sperimentâl di %2$s, e je integrade tal navigadôr. Doprant la tecnologjie di %1$s di Mozilla, ti judìn a evitâ recensions di part e falsis. Il nestri model di inteligjence artificiâl al miore in continuazion par proteziti intant che tu fasis compris. %2$s @@ -2368,4 +2375,6 @@ lei l’articul vierzi il colegament par savê di plui + + %s, intestazion diff --git a/app/src/main/res/values-fy-rNL/strings.xml b/app/src/main/res/values-fy-rNL/strings.xml index f08abcfdc6..a58d89b171 100644 --- a/app/src/main/res/values-fy-rNL/strings.xml +++ b/app/src/main/res/values-fy-rNL/strings.xml @@ -462,16 +462,20 @@ Allinnich-HTTPS-modus - Reduksje fan cookiebanners + Reduksje fan cookiebanners + + Blokkearring fan cookiebanners + + Blokkearring fan cookiebanners wylst priveenavigaasje - Cookiebanners redusearje + Cookiebanners redusearje - Ut + Ut - Oan + Oan - %1$s probearret automatysk cookiefersiken op cookiebanners te wegerjen. + %1$s probearret automatysk cookiefersiken op cookiebanners te wegerjen. Ut foar dizze website @@ -489,16 +493,24 @@ Website wurdt op dit stuit net stipe - Reduksje fan cookiebanners ynskeakelje foar %1$s? + Reduksje fan cookiebanners ynskeakelje foar %1$s? + + Blokkearring fan cookiebanners ynskeakelje foar %1$s? + + Reduksje fan cookiebanners útskeakelje foar %1$s? - Reduksje fan cookiebanners útskeakelje foar %1$s? + Blokkearring fan cookiebanners útskeakelje foar %1$s? %1$s kin cookiefersiken op dizze website net automatysk wegerje. Jo kinne in oanfraach stjoere om dizze website yn de takomst te stypjen. - %1$s wisket de cookies foar dizze website en fernijt de side. As alle cookies wiske wurde, wurde jo mooglik ôfmeld of wurde winkelweintsjes lege. + %1$s wisket de cookies foar dizze website en fernijt de side. As alle cookies wiske wurde, wurde jo mooglik ôfmeld of wurde winkelweintsjes lege. + + Skeakelje dit út en %1$s sil cookies wiskje en dizze website opnij lade. Dit kin jo ôfmelde of winkelweintsjes leegje. - %1$s probearret alle cookiefersiken op stipe websites automatysk te wegerjen. + %1$s probearret alle cookiefersiken op stipe websites automatysk te wegerjen. + + Skeakelje dit yn en %1$s sil probearje cookiebanners op dizze website automatysk te wegerjen. %1$s tastean om cookiebanners te wegerjen? @@ -511,6 +523,11 @@ Tastean + + %1$s hat sakrekt cookies foar jo wegere + + Minder ôfliedingen, minder cookies dy’t jo folgje op dizze website. + Probearret foar in bettere befeiliging automatysk mei it HTTPS-fersiferingsprotokol ferbining te meitsjen mei websites. @@ -621,6 +638,8 @@ Add-ons + + Add-on ynstallearje fia bestân Notifikaasjes @@ -2227,8 +2246,6 @@ Dizze analyze sil jo allinnich helpe om de beoardielingskwaliteit te beoardielen Advertinsje fan %s - Beoardielingskontrôle wurdt mooglik makke troch %s. - Beoardielingskontrôle is mooglik makke troch %s %s troch Mozilla @@ -2289,6 +2306,8 @@ Dizze analyze sil jo allinnich helpe om de beoardielingskwaliteit te beoardielen Probearje ús fertroude gids foar produktbeoardielingen Besjoch hoe betrouber de produktbeoardielingen op %1$s binne eardat jo keapje. Beoardielingskontrôle, in eksperimintele funksje fan %2$s, is streekrjocht yn de browser ynboud. It wurket ek op %3$s en %4$s. + + Besjoch hoe betrouber de produktbeoardielingen op %1$s binne eardat jo keapje. Beoardielingskontrôle, in eksperimintele funksje fan %2$s, is streekrjocht yn de browser ynboud. Mei help fan de krêft fan %1$s troch Mozilla helpe wy jo befoaroardiele en net-autentike beoardielingen foar te kommen. Us AI-model wurdt hieltyd ferbettere om jo te beskermjen wylst jo winkelje. %2$s @@ -2356,4 +2375,6 @@ Dizze analyze sil jo allinnich helpe om de beoardielingskwaliteit te beoardielen it artikel te lêzen de keppeling te iepenjen foar mear ynfo + + %s, koptekst diff --git a/app/src/main/res/values-gn/strings.xml b/app/src/main/res/values-gn/strings.xml index 6acfebe33f..eb1969b726 100644 --- a/app/src/main/res/values-gn/strings.xml +++ b/app/src/main/res/values-gn/strings.xml @@ -488,7 +488,7 @@ Emondo mba’ejerure - ¿Ejerure pytyvõ ko tendápe g̃uara? + ¿Ejerure pytyvõ ko tendápe g̃uarã? Mba’ejerure mondopyre @@ -529,6 +529,12 @@ Moneĩ + + %1$s omboyke kookie nde rehehápe + + + Sa’ive ñakãity, sa’ive kookie nde rapykuehóva ko tendápe. + Eñeha’ã eike hag̃ua tendakuérape eiporúvo pe taperekoite HTTPS ipapapýva tekorosãverã. @@ -671,7 +677,7 @@ Ojeikeramohague - Tembiasakue nemyakãngetáva + Tembiasakue nemoakãngetáva Jehaipy oykekóva %s @@ -1363,7 +1369,7 @@ Emondo hag̃ua tendayke, eñepyrũ tembiapo Firefox-pe ambue mba’e’okápe. - Kumbypyre + Kũmbypyre Ndaikatúi emoherakuã ko tembiporu’i ndive @@ -2127,7 +2133,7 @@ Ndaipóri tendayke ijurujáva - Emyasãi tendayke aty mbojuehepyre + Emoasãi tendayke aty mbojuehepyre Emokañy tendayke aty mbojuehepyre @@ -2137,7 +2143,7 @@ Embojuaju hag̃ua jeike pyahu, embogue peteĩ. Ejopy are tenda ha eiporavo mboguete. - Oĩma, aikumby + Oĩma, aikũmby Jeike pya’eha @@ -2164,7 +2170,7 @@ Embotypaite tendayke ojeporu’ỹva - Emyasãi tendayke ojeporu’ỹva + Emoasãi tendayke ojeporu’ỹva Emomichĩ tendayke ojeporu’ỹva @@ -2206,7 +2212,7 @@ - Tembiasakue nemyakãngetáva + Tembiasakue nemoakãngetáva Tembiasakue téma rehegua @@ -2278,7 +2284,7 @@ Ehecháta ñemurã sapy’agua apopyre iporãvagui. Opaite ñemurã ojapova’erã oipotaháicha jehechajey porãverã. %s - Ehecháta ñemurã sapy’apy’a apopyre oikóva rehegua. Romyasãi ñemurã apopyre jeroviaha añoite. %s + Ehecháta ñemurã sapy’apy’a apopyre oikóva rehegua. Romoasãi ñemurã apopyre jeroviaha añoite. %s Eikuaave @@ -2288,8 +2294,6 @@ Ñemurã %s - - Marandu’i rechajeyhápe oiko %s rupive. Marandu’i rechajeyhápe oiko %s rupive. @@ -2323,7 +2327,7 @@ Ndorohechajeykuaái ko marandu’i. - Rombyasy, ndorohechajeykuaái marandu’i porãngue peteĩchagua apopyrépe. Techapyrã, jopói kuatia’atã ha ta’ãngamýi ñemyasãi, purahéi ha ñembosarái. + Rombyasy, ndorohechajeykuaái marandu’i porãngue peteĩchagua apopyrépe. Techapyrã, jopói kuatia’atã ha ta’ãngamýi ñemoasãi, purahéi ha ñembosarái. Marandu ojeporupotáma @@ -2331,7 +2335,7 @@ Ñehesa’ỹijo hekopyahúma - Aikumby + Aikũmby Ndaipóri marandu ko’ág̃aite @@ -2408,13 +2412,13 @@ momichĩmbyre - myasãi + moasãi - myasãimbyre + moasãimbyre embojuruja juajuha eikuaave hag̃ua ko ñembyaty emoñe’ẽ jehaipy embojuruja juajuha eikuaave hag̃ua - + diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index ddb6ac2bb1..3c5ffe7fe1 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -207,6 +207,8 @@ Biblioteka Klasični prikaz + + Otvori u redovnoj kartici Dodaj na početni ekran @@ -264,7 +266,7 @@ Skeniraj - Tražilica + Tražilica Traži postavke tražilice @@ -334,6 +336,8 @@ Firefox stavlja ljude iznad profita i brani vašu privatnost blokiranjem alata za praćenje.\n\nSaznajte više u našoj politici privatnosti. + + Naš neprofitni preglednik pomaže spriječiti tvrtke da vas tajno prate na webu.\n\nSaznajte više u našoj obavijesti o privatnosti. politika privatnosti @@ -343,6 +347,8 @@ Ne sada Prebacite se s telefona na laptop i nazad + + Ostanite zaštićeni šifriranjem kada prenosite podatke između uređaja Dohvatite otvorene kartice i lozinke s vašeg drugog uređaja i nastavite gdje ste stali. @@ -360,6 +366,12 @@ Ne sada + + Dodaj Firefox widget + + Ne sada + Otvori novu %1$s karticu @@ -421,16 +433,16 @@ Način rada "Samo HTTPS" - Smanjivanje pojavljivanja dijaloga kolačića + Smanjivanje pojavljivanja dijaloga kolačića - Smanji pojavljivanje dijaloga kolačića + Smanji pojavljivanje dijaloga kolačića - Isključeno + Isključeno - Uključeno + Uključeno - %1$s pokušava automatski odbiti zahtjeve za kolačiće na stranicama koje to traže. + %1$s pokušava automatski odbiti zahtjeve za kolačiće na stranicama koje to traže. Isključi za ovu stranicu @@ -449,25 +461,25 @@ Stranica trenutno nije podržana - Uključi smanjivanje pojavljivanja dijaloga kolačića za %1$s? + Uključi smanjivanje pojavljivanja dijaloga kolačića za %1$s? - Isključi smanjivanje pojavljivanja dijaloga kolačića za %1$s? + Isključi smanjivanje pojavljivanja dijaloga kolačića za %1$s? %1$s ne može automatski odbiti zahtjeve za kolačićima na ovoj stranici. Možete poslati zahtjev da se podrži ova stranica u budućnosti. - %1$s će izbrisati kolačiće stranice i osvježiti stranicu. Čišćenje svih kolačića će vas možda odjaviti sa stranice ili isprazniti košaricu. + %1$s će izbrisati kolačiće stranice i osvježiti stranicu. Čišćenje svih kolačića će vas možda odjaviti sa stranice ili isprazniti košaricu. - %1$s pokušava automatski odbiti sve zahtjeve za kolačićima na podržanim stranicama. + %1$s pokušava automatski odbiti sve zahtjeve za kolačićima na podržanim stranicama. - Dopusti %1$s za odbije dijaloge za kolačiće? + Dopusti %1$s za odbije dijaloge za kolačiće? - %1$s može automatski odbiti mnoge zahtjeve za kolačićima. + %1$s može automatski odbiti mnoge zahtjeve za kolačićima. - Ne sada + Ne sada - Vidjet ćete manje zahtjeva za kolačićima + Vidjet ćete manje zahtjeva za kolačićima - Dozvoli + Dozvoli Automatski se pokušava povezati s web stranicama pomoću HTTPS protokola za šifriranje radi povećane sigurnosti. @@ -625,16 +637,10 @@ Klasični %s - - Ograničeno izdanje Umjetnička serija - - Nova kolekcija nezavisnih glasova. %s Kolekcija nezavisnih glasova. %s - - Nova kolekcija nezavisnih glasova. Kolekcija nezavisnih glasova. @@ -1386,14 +1392,11 @@ %d kartica(e) - Povijest pregledavanja i podaci web-stranica Povijest pregledavanja %d adresa(e) - - Kolačići Kolačići i podaci web stranice @@ -1449,65 +1452,10 @@ Grupa obrisana - - Dobrodošli na bolji Internet - - Preglednik napravljen za ljude, ne za profit. - - Nastavite tamo gdje ste stali - - Sinkronizirajte kartice i lozinke na svim uređajima za besprijekorno prebacivanje između uređaja. - - Prijavi se Sinkronizacija je uključena - - Zaštita privatnosti kao standard - - %1$s automatski sprječava tvrtke da te potajno prate širom weba. - - Sadrži potpunu zaštitu od kolačića kako bi spriječio programe za praćenje u korištenju kolačića da vas prate između web stranica. - - Standardno (zadano) - - Uravnotežena privatnost i performanse. Stranice se normalno učitavaju. - - Strogo - - Blokira više programa za praćenje pa se stranice učitavaju brže, ali neke funkcionalnosti stranica možda će se slomiti. - - Odaberi položaj alatne trake - - - Držite ga na dnu ili ga premjestite na vrh. - - Vi kontrolirate svoje podatke - - Firefox vam daje kontrolu nad onim što dijelite na mreži i nad onim što dijelite s nama. - - Pročitaj naša pravila privatnosti - - - Jeste li spremni otkriti nevjerojatan Internet? - - Započni surfati - - - Odaberi modus - - - Štedi bateriju i zaštiti oči pomoću tamne teme. - - Automatski - - Prilagođava se postavkama uređaja - - Tamna tema - - Svijetla tema - Kartice su poslane! @@ -1958,8 +1906,6 @@ URL API za prijedloge pretraživanja - Zamijenite upit s “%s”. Na primjer:\nhttp://suggestqueries.google.com/complete/search?client=firefox&q=%s - Zamijenite upit s “%s”. Na primjer:\nhttps://suggestqueries.google.com/complete/search?client=firefox&q=%s Spremi diff --git a/app/src/main/res/values-hsb/strings.xml b/app/src/main/res/values-hsb/strings.xml index 410bbdb663..0e702b688a 100644 --- a/app/src/main/res/values-hsb/strings.xml +++ b/app/src/main/res/values-hsb/strings.xml @@ -521,6 +521,11 @@ Dowolić + + %1$s je runje placki za was wotpokazał + + Mjenje wotwjedźenjow, mjenje plackow, kotrež was na sydle přesćěhuja. + Pospytuje z pomocu zaklučowanskeho protokola HTTPS za powyšenu wěstotu awtomatisce ze sydłami zwjazać. @@ -630,6 +635,8 @@ Přidatki + + Přidatk z dataje instalować Zdźělenki @@ -2248,8 +2255,6 @@ To budźe jenož pomhać, kwalitu pohódnoćenjow posudźić, nic kwalitu produk Wabjenje wot %s - Kontrola pohódnoćenjow so wot %s spěchuje. - Kontrola pohódnoćenjow so wot %s spěchuje %s wot Mozilla @@ -2309,6 +2314,8 @@ To budźe jenož pomhać, kwalitu pohódnoćenjow posudźić, nic kwalitu produk Čitajće naš dowěry hódny přewodnik wo pohódnoćenjach produktow Hladajće, kak spušćomne pohódnoćenja produktow na %1$s su, prjedy hač produkty kupiće. Kontrola pohódnoćenjow, eksperimentalna funkcija z %2$s, je direktnje do wobhladowaka zatwarjena. Funguje tež na %3$s a %4$s. + + Hladajće, kak spušćomne pohódnoćenja produktow na %1$s su, prjedy hač produkty kupiće. Kontrola pohódnoćenjow, eksperimentalna funkcija z %2$s, je direktnje do wobhladowaka zatwarjena. Z pomocu potenciala %1$s wot Mozilla, pomhamy wam, předzajate a njeawtentiske pohódnoćenja wobeńć. Naš model KI so stajnje polěpšuje, zo by was škitał, hdyž nakupujeće. %2$s @@ -2376,4 +2383,6 @@ To budźe jenož pomhać, kwalitu pohódnoćenjow posudźić, nic kwalitu produk nastawk čitać wotkaz wočinić, zo byšće wjace zhonił + + %s, nadpismo diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 16b53d2541..58f7de0c4a 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -206,6 +206,8 @@ Könyvtár Asztali oldal + + Megnyitás szokásos lapon Kezdőképernyőhöz adás @@ -460,16 +462,20 @@ Csak HTTPS mód - Sütibannerek számának csökkentése + Sütibannerek számának csökkentése + + Sütibanner-blokkoló + + Sütibanner-blokkoló privát böngészésben - A sütibannerek számának csökkentése + A sütibannerek számának csökkentése - Ki + Ki - Be + Be - A %1$s automatikusan megpróbálja elutasítani a sütibannereken lévő sütikéréseket. + A %1$s automatikusan megpróbálja elutasítani a sütibannereken lévő sütikéréseket. Kikapcsolva erre az oldalra @@ -487,16 +493,24 @@ A webhely jelenleg nem támogatott - Bekapcsolja a sütibanner-csökkentést a következőnél: %1$s? + Bekapcsolja a sütibanner-csökkentést a következőnél: %1$s? + + Bekapcsolja a sütibanner-blokkolót ennél a webhelynél: %1$s? + + Kikapcsolja a sütibanner-csökkentést a következőnél: %1$s? - Kikapcsolja a sütibanner-csökkentést a következőnél: %1$s? + Kikapcsolja a sütibanner-blokkolót ennél a webhelynél: %1$s? A %1$s nem tudja automatikusan elutasítani a sütikéréseket ezen az oldalon. Küldhet egy kérést, hogy támogassák ezt az oldalt a jövőben. - A %1$s törli a webhely sütijeit, és frissíti az oldalt. Az összes süti törlésével kijelentkezhet, vagy kiürítheti a kosarait. + A %1$s törli a webhely sütijeit, és frissíti az oldalt. Az összes süti törlésével kijelentkezhet, vagy kiürítheti a kosarait. + + Kapcsolja ki, és a %1$s törli a sütiket, és újratölti a webhelyet. Ez kijelentkeztetheti, vagy kiürítheti a kosarait. - A %1$s automatikusan megpróbálja elutasítani az összes sütikérést a támogatott oldalakon. + A %1$s automatikusan megpróbálja elutasítani az összes sütikérést a támogatott oldalakon. + + Kapcsolja be, és a %1$s automatikusan megpróbálja elutasítani a sütibannereket ezen az oldalon. Engedélyezi a %1$s számára a sütibannerek elutasítását? @@ -509,6 +523,11 @@ Engedélyezés + + A %1$s most elutasította a sütiket Önnek + + Kevesebb zavaró tényező, kevesebb süti, amely követné ezen az oldalon. + Automatikusan HTTPS titkosítási protokoll használatával próbál meg csatlakozni a webhelyekhez a fokozott biztonság érdekében. @@ -620,6 +639,8 @@ Kiegészítők + + Kiegészítő telepítése fájlból Értesítések @@ -2239,8 +2260,6 @@ Reklám a következőtől: %s - Az értékelés-ellenőrzőt a %s biztosítja. - Az értékelés-ellenőrzőt a %s biztosítja %s by Mozilla @@ -2303,6 +2322,8 @@ Próbálja ki megbízható termékértékelési útmutatónkat Vásárlás előtt nézze meg, hogy mennyire megbízhatók a %1$s termékértékelései. Az értékelés-ellenőrző, a %2$s egy kísérleti funkciója, közvetlenül a böngészőbe van építve. A következőkön is működik: %3$s és %4$s. + + Vásárlás előtt nézze meg, hogy mennyire megbízhatók a %1$s termékértékelései. Az értékelés-ellenőrző, a %2$s egy kísérleti funkciója, közvetlenül a böngészőbe van építve. A %1$s by Mozilla erejét használva segítünk elkerülni az elfogult és a nem hiteles értékeléseket. Az MI modellünket folyamatosan fejlesztjük, hogy megvédjük Önt vásárlás közben. %2$s @@ -2371,4 +2392,6 @@ a cikk elolvasása hivatkozás megnyitása, hogy többet tudjon meg + + %s, címsor diff --git a/app/src/main/res/values-hy-rAM/strings.xml b/app/src/main/res/values-hy-rAM/strings.xml index 5d872223bc..e1fe61b0ca 100644 --- a/app/src/main/res/values-hy-rAM/strings.xml +++ b/app/src/main/res/values-hy-rAM/strings.xml @@ -205,6 +205,8 @@ Գրադարան Դեսքթոփ տարբերակ + + Բացել կանոնավոր ներդիրում Ավելացնել Տնային էկրանին @@ -456,16 +458,20 @@ HTTPS կերպ միայն - Թխուկների դրոշակի կրճատում + Թխուկների դրոշակի կրճատում + + Թխուկների ազդերիզի արգելափակիչ + + Թխուկների ազդերիզի արգելափակիչ մասնավոր զննարկումում - Նվազեցնել թխուկների պաստառները + Նվազեցնել թխուկների պաստառները - Անջ. + Անջ. - Միաց. + Միաց. - %1$s-ն ինքնաբար փորձում է մերժել թխուկների հարցումները թխուկների ցուցանակների վրա: + %1$s-ն ինքնաբար փորձում է մերժել թխուկների հարցումները թխուկների ցուցանակների վրա: Անջատված է այս կայքի համար @@ -483,16 +489,24 @@ Կայքը ներկայումս չի աջակցվում - Միացնե՞լ Cookie Banner կրճատումը %1$s-ի համար: + Միացնե՞լ Cookie Banner կրճատումը %1$s-ի համար: + + Միացնե՞լ Թխուկների ազդերիզի արգելափակիչը %1$s-ում: + + Անջատե՞լ Cookie Banner կրճատումը %1$s-ի համար: - Անջատե՞լ Cookie Banner կրճատումը %1$s-ի համար: + Անջատե՞լ Թխուկների ազդերիզի արգելափակիչը %1$s-ում: %1$s-ը չի կարող ինքնաշխատ մերժել թխուկների հարցումներն այս կայքում: Դուք կարող եք հարցում ուղարկել այս կայքին ապագայում աջակցելու համար: - %1$s-ը կջնջի այս կայքի թխուկները և կթարմացնի էջը: Բոլոր թխուկները մաքրելը կարող է դուրս գրել Ձեզ կամ դատարկել գնումների զամբյուղները: + %1$s-ը կջնջի այս կայքի թխուկները և կթարմացնի էջը: Բոլոր թխուկները մաքրելը կարող է դուրս գրել Ձեզ կամ դատարկել գնումների զամբյուղները: + + Անջատեք և %1$s-ը կմաքրի թխուկները և կրկին կբեռնի այս կայքը: Դա կարող է ձեզ դուրս գրել կամ դատարկել գնումների զամբյուղը: - %1$s-ը փորձում է ինքնաբար մերժել թխուկների հարցումները աջակցվող կայքերում: + %1$s-ը փորձում է ինքնաբար մերժել թխուկների հարցումները աջակցվող կայքերում: + + Միացրեք և %1$s-ը ինքնաբար կմերժի թխուկների բոլոր ազդերիզները այս կայքում: Թույլատրե՞լ %1$s-ին մերժել թխուկների պաստառները: @@ -505,6 +519,11 @@ Թույլատրել + + %1$s-ը հենց նոր մերժեց թխուկները ձեզ համար + + Ավելի քիչ շեղումներ, ավելի քիչ թխուկներ, որոնք հետևում են ձեզ այս կայքում: + Ինքնաշխատ կերպով փորձում է միանալ կայքերին՝ օգտագործելով HTTPS գաղտնագրման արձանագրությունը՝ անվտանգության բարձրացման համար: @@ -615,6 +634,8 @@ Հավելումներ + + Տեղադրեք հավելումը ֆայլից Ծանուցումներ @@ -2221,8 +2242,6 @@ Գովազդ %s-ի կողմից - Կարծիքների ստուգիչն աշխատում է %s-ի կողմից: - Կարծիքների ստուգիչը ստեղծված է %s-ի կողմից %s Mozilla-ի կողմից @@ -2282,6 +2301,8 @@ Փորձեք արտադրանքի կարծիքների մեր վստահելի ուղեցույցը Գնելուց առաջ տեսեք, թե որքան հուսալի են արտադրանքի կարծիքները %1$s-ում: Կարծիքների ստուգիչը՝ %2$s-ի փորձարարական յուրահատկությունն է և ներկառուցված է հենց զննիչում: Այն աշխատում է նաև %3$s-ի և %4$s-ի վրա: + + Գնելուց առաջ տեսեք, թե որքան հուսալի են արտադրանքի կարծիքները %1$s-ում: Կարծիքների ստուգիչը %2$s-ի փորձարարական յուրահատկությունն է և ներկառուցված է հենց զննիչում: Օգտագործելով %1$s-ի հզորությունը Mozilla-ի կողմից՝ մենք օգնում ենք ձեզ խուսափել կողմնակալ և ոչ վավերական կարծիքներից: Մեր AI մոդելը միշտ բարելավվում է՝ պաշտպանելու ձեզ գնումներ կատարելիս: %2$s @@ -2349,4 +2370,6 @@ կարդալ հոդվածը բացեք հղումը՝ ավելին իմանալու համար + + %s, վերնագիր diff --git a/app/src/main/res/values-ia/strings.xml b/app/src/main/res/values-ia/strings.xml index f980028217..3a9e39aec7 100644 --- a/app/src/main/res/values-ia/strings.xml +++ b/app/src/main/res/values-ia/strings.xml @@ -511,8 +511,12 @@ %1$s clarara le cookies de iste sito e actualisara le pagina. Clarar tote le cookies pote clauder tu connexion o vacuar tu carrettos de compras. + + Disactiva lo e %1$s clarara le cookies e recargara iste sito. Isto pote disconnecter le section o vacuara le carrettos de compras. %1$s tenta rejectar automaticamente tote le requestas de cookies sur le sitos supportate. + + Activa lo e %1$s essayara refusar automaticamente le bandieras pro cookies sur iste sito. Permitter a %1$s de rejectar bandieras pro cookies? @@ -526,6 +530,11 @@ Permitter + + %1$s justo refusava cookies pro te + + Minus distractiones, minus cookies traciante te sur iste sito. + Automaticamente tenta de connecter se al sitos per le protocollo de cryptation HTTPS pro major securitate. @@ -635,6 +644,8 @@ Additivos + + Installar additivo ab un file Notificationes @@ -2292,8 +2303,6 @@ Aviso publicitari per %s - Le verificator de recension es supportate per %s. - Le verificator de recension es supportate per %s %s per Mozilla @@ -2354,6 +2363,8 @@ Prova nostre guida digne de fide pro recensiones de producto Vide quanto fidabile es le recensiones de producto sur %1$s ante que tu compra. Verificator de recension, un function experimental de %2$s, es producite justo in le navigator. Illo ancora functiona sur %3$s e %4$s. + + Vide quanto fidabile es le recensiones de producto sur %1$s ante que tu compra. Verificator de recension, un function experimental per %2$s, es integrate justo in le navigator. Per le potentia de %1$s per Mozilla, nos te adjuta a evitar recensiones prevenite e inauthentic. Nostre modello de intelligentia artificial sempre meliora pro proteger te dum tu compra. %2$s @@ -2420,4 +2431,6 @@ leger le articulo aperi le ligamine pro saper plus + + %s, Titulo diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 68024e8384..22acc1e75b 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -89,6 +89,7 @@ Pelajari tentang Perlindungan Kuki Total + Akses kamera diperlukan. Buka pengaturan Android, ketuk izin, dan ketuk izinkan. @@ -239,7 +240,7 @@ Pindai - Mesin pencari + Mesin pencari Setelan mesin pencari @@ -306,19 +307,12 @@ Jangan sekarang - - Jadikan %s sebagai peramban pilihan Anda - Jadikan Firefox sebagai peramban pilihan Anda - - %1$s mengutamakan masyarakat daripada laba dan membela privasi Anda dengan memblokir pelacak lintas situs.\n\n\nPelajari lebih lanjut dalam %2$s. + Jadikan Firefox sebagai peramban pilihan Anda - Firefox mengutamakan masyarakat daripada laba dan membela privasi Anda dengan memblokir pelacak lintas situs.\n\n\nPelajari lebih lanjut dalam kebijakan privasi kami. + Firefox mengutamakan masyarakat daripada laba dan membela privasi Anda dengan memblokir pelacak lintas situs.\n\n\nPelajari lebih lanjut dalam kebijakan privasi kami. kebijakan privasi @@ -327,26 +321,20 @@ Jangan sekarang - Berpindah dari ponsel ke laptop dan sebaliknya + Berpindah dari ponsel ke laptop dan sebaliknya - Bawa tab dan kata sandi Anda dari perangkat lain untuk meneruskan jelajah terakhir. + Bawa tab dan kata sandi Anda dari perangkat lain untuk meneruskan jelajah terakhir. Masuk Jangan sekarang - - Notifikasi membantu Anda lakukan lebih banyak hal dengan %s - Notifikasi membantu Anda lakukan lebih banyak hal dengan Firefox + Notifikasi membantu Anda lakukan lebih banyak hal dengan Firefox - - Kirim tab antar perangkat, kelola unduhan, dan dapatkan kiat terbaik menggunakan %s. - Kirim tab antar perangkat, kelola unduhan, dan dapatkan kiat terbaik menggunakan Firefox. + Note: The word "Firefox" should NOT be translated --> + Kirim tab antar perangkat, kelola unduhan, dan dapatkan kiat terbaik menggunakan Firefox. Aktifkan notifikasi @@ -373,7 +361,7 @@ Pilih satu - Kelola pintasan pencarian + Kelola pintasan pencarian Sunting mesin peramban terlihat di menu pencarian @@ -383,7 +371,7 @@ Cari - Bilah alamat + Bilah alamat Beri nilai di Google Play - Pengurangan Spanduk Kuki + Pengurangan Spanduk Kuki + + Pemblokir Spanduk Kuki - Kurangi spanduk kuki + Kurangi spanduk kuki - Nonaktif + Nonaktif - Aktif + Aktif - %1$s secara otomatis coba menolak permintaan kuki di spanduk kuki. + %1$s secara otomatis coba menolak permintaan kuki di spanduk kuki. Nonaktif untuk situs ini Batal - Minta dukungan - Kirim permintaan Minta dukungan untuk situs ini? @@ -443,27 +431,27 @@ Saat ini, situs tidak didukung - Aktifkan Pengurangan Spanduk Kuki untuk %1$s? + Aktifkan Pengurangan Spanduk Kuki untuk %1$s? - Nonaktifkan Pengurangan Spanduk Kuki untuk %1$s? + Nonaktifkan Pengurangan Spanduk Kuki untuk %1$s? %1$s tidak dapat secara otomatis menolak permintaan kuki di situs ini. Anda dapat mengirimkan permintaan dukungan untuk situs ini di masa mendatang. - %1$s akan menghapus kuki situs ini dan menyegarkan laman ini. Membersihkan semua kuki dapat membuat Anda keluar dari suatu situs atau mengosongkan keranjang belanja. + %1$s akan menghapus kuki situs ini dan menyegarkan laman ini. Membersihkan semua kuki dapat membuat Anda keluar dari suatu situs atau mengosongkan keranjang belanja. - %1$s mencoba secara otomatis menolak semua permintaan kuki di situs yang didukung. + %1$s mencoba secara otomatis menolak semua permintaan kuki di situs yang didukung. - Izinkan %1$s untuk menolak spanduk kuki? + Izinkan %1$s untuk menolak spanduk kuki? - %1$s dapat menolak permintaan spanduk kuki secara otomatis. + %1$s dapat menolak permintaan spanduk kuki secara otomatis. - Jangan sekarang + Jangan sekarang - Anda akan melihat lebih sedikit permintaan kuki + Anda akan melihat lebih sedikit permintaan kuki - Izinkan + Izinkan Secara otomatis mencoba terhubung ke situs menggunakan protokol enkripsi HTTPS untuk meningkatkan keamanan. @@ -488,11 +476,11 @@ Aksesibilitas - Penyedia Firefox Account khusus + Penyedia Firefox Account khusus Penyedia Sync khusus - Penyedia Firefox Account/Sync dimodifikasi. Keluar dari aplikasi untuk menerapkan pengubahan… + Penyedia Firefox Account/Sync dimodifikasi. Keluar dari aplikasi untuk menerapkan pengubahan… Akun @@ -508,7 +496,7 @@ Masuk untuk menyinkronkan tab, markah, sandi, dan yang lainnya. - Firefox Account + Firefox Account Hubungkan kembali untuk melanjutkan sinkronisasi @@ -520,7 +508,7 @@ Pengawakutuan jarak jauh melalui USB - Tampilkan mesin pencari + Tampilkan mesin pencari Tampilkan saran pencarian @@ -620,12 +608,6 @@ %s Klasik - - Edisi Terbatas - - Koleksi Suara Independen baru. %s - - Koleksi Suara Independen baru. Coba berbagai ragam warna @@ -635,9 +617,9 @@ - Pengaya tidak didukung + Pengaya tidak didukung - Pengaya telah didukung + Pengaya telah didukung @@ -939,9 +921,9 @@ Nama koleksi - Ubah nama - - Hapus + Ubah nama + + Hapus Hapus dari riwayat @@ -1220,7 +1202,7 @@ Tutup - Gagal mencetak + Gagal mencetak Cetak @@ -1363,14 +1345,10 @@ Tab terbuka %d tab - - Riwayat penjelajahan dan situs data %d alamat situs - - Kuki Anda\ akan keluar dari sebagian besar situs @@ -1426,62 +1404,10 @@ Grup dihapus - - Selamat datang di Internet yang lebih baik - - Peramban yang dibuat untuk masyarakat, bukan profit. - - Mulai dari situasi saat Anda pergi - - Sinkronkan tab dan kata sandi di berbagai perangkat untuk peralihan layar yang mulus. - - Masuk Sinkronisasi aktif - - Perlindungan privasi secara bawaan - - %1$s secara otomatis menghentikan perusahaan yang mengikuti Anda di web secara rahasia. - - Menghadirkan Perlindungan Kuki Total untuk mencegah pelacak gunakan kuki untuk menguntit Anda di web. - - Standar (baku) - - Seimbang untuk privasi dan kinerja. Halaman dimuat secara normal. - - Ketat - - Blokir lebih banyak pelacak sehingga laman dimuat lebih cepat, tetapi beberapa fungsionalitas pada laman mungkin rusak. - - Pilih penempatan bilah alat Anda - - Simpan di bawah, atau pindahkan ke atas. - - Anda mengendalikan data Anda - - Firefox memberi Anda kendali atas apa saja yang Anda bagikan secara daring dan apa yang Anda bagikan kepada kami. - - Pelajari pemberitahuan privasi kami - - - Siap membuka Internet yang luar biasa? - - Mulai menjelajah - - Pilih tema Anda - - Hemat baterai dan penglihatan Anda dengan mode gelap. - - Otomatis - - Menyesuaikan dengan pengaturan perangkat Anda - - Tema gelap - - Tema terang - Tab berhasil dikirim! @@ -1888,26 +1814,26 @@ Ubah mesin pencari - Tambah + Tambah - Simpan + Simpan Ubah Hapus - Lainnya + Lainnya Nama - Nama + Nama Nama mesin pencari URL string pencarian - String pencarian untuk digunakan + String pencarian untuk digunakan URL yang digunakan untuk pencarian @@ -1920,8 +1846,6 @@ API saran pencarian (opsional) URL API saran pencarian - - Ganti kueri dengan “%s”. Contoh:\nhttp://suggestqueries.google.com/complete/search?client=firefox&q=%s Simpan @@ -2052,14 +1976,14 @@ Pintasan - - Nama + + Nama Nama pintasan - - OK - - Batal + + OK + + Batal Pengaturan @@ -2147,4 +2071,4 @@ baca artikel buka tautan untuk mempelajari lebih lanjut - + diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index ab25af86b1..8252aa0efe 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -522,6 +522,11 @@ Leyfa + + %1$s var að hafna vefkökum fyrir þína hönd + + Minni truflanir, færri vefkökur sem rekja ferðir þínar á þessu vefsvæði. + Reynir sjálfkrafa að tengjast vefsvæðum með HTTPS dulritunareglum til að auka öryggi. @@ -630,6 +635,8 @@ Viðbætur + + Setja inn viðbót úr skrá Tilkynningar @@ -2234,8 +2241,6 @@ Auglýsing frá %s - Umsagnaskoðunin er keyrð af %s. - Umsagnaskoðunin er keyrð af %s %s frá Mozilla @@ -2295,6 +2300,8 @@ Prófaðu traustar leiðbeiningar okkar um vöruumsagnir Sjáðu hversu áreiðanlegar vöruumsagnir eru á %1$s áður en þú kaupir. Umsagnaskoðun, tilraunaeiginleiki frá %2$s, er innbyggður beint inn í vafrann. Það virkar á %3$s og einnig %4$s. + + Sjáðu hversu áreiðanlegar vöruumsagnir eru á %1$s áður en þú kaupir. Umsagnaskoðun, tilraunaeiginleiki frá %2$s, er innbyggður beint inn í vafrann. Með hjálp %1$s frá Mozilla, gerum við þér kleift að forðast hlutdrægar og ósannar umsagnir. Gervigreindarlíkanið okkar er alltaf að batna til að vernda þig sem best þegar þú verslar. %2$s @@ -2361,4 +2368,6 @@ lesa greinina opnaðu tengilinn til að fræðast nánar + + %s, Fyrirsögn diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 2aeeb93581..4ade559cbd 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -528,6 +528,11 @@ Consenti + + %1$s ha appena rifiutato dei cookie per te + + Meno distrazioni, meno cookie in grado di tracciarti su questo sito. + Tenta automaticamente la connessione ai siti utilizzando il protocollo di crittografia HTTPS per una maggiore sicurezza. @@ -639,6 +644,8 @@ Componenti aggiuntivi + + Installa componente aggiuntivo da file Notifiche @@ -2281,8 +2288,6 @@ Altri prodotti da valutare Annuncio di %s - - Verifica recensioni con tecnologia %s. Verifica recensioni con tecnologia %s @@ -2347,6 +2352,8 @@ Prova la nostra guida alle recensioni di prodotti Scopri quanto sono affidabili le recensioni dei prodotti su %1$s prima di acquistarli. Verifica recensioni, una funzione sperimentale di %2$s, è integrata direttamente nel browser. Funziona anche su %3$s e %4$s. + + Scopri quanto sono affidabili le recensioni dei prodotti su %1$s prima di acquistarli. Verifica recensioni, una funzione sperimentale di %2$s, è integrata direttamente nel browser. Utilizzando la tecnologia di %1$s by Mozilla, ti aiutiamo a evitare recensioni di parte e non autentiche. Il nostro modello di intelligenza artificiale migliora costantemente per proteggerti mentre fai acquisti. %2$s @@ -2413,4 +2420,6 @@ leggere l’articolo aprire il link con ulteriori informazioni + + %s, intestazione diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 1f1a970eba..a0856158a4 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -559,6 +559,8 @@ תוספות + + התקנת תוספת מקובץ התרעות @@ -2166,8 +2168,6 @@ פרסומת מאת %s - בודק הסקירות מופעל על־ידי %s. - בודק הסקירות מופעל על־ידי %s %s מאת Moziila @@ -2229,6 +2229,8 @@ נסו את המדריך המהימן שלנו לסקירות מוצרים ניתן לבדוק עד כמה ביקורות מוצר אמינות ב־%1$s לפני הקנייה. בודק הסקירות, תכונה ניסיונית מאת %2$s, מובנה ישירות בדפדפן. הוא עובד גם על %3$s ו־%4$s. + + ניתן לבדוק עד כמה ביקורות מוצר אמינות ב־%1$s לפני הקנייה. בודק הסקירות, תכונה ניסיונית מאת %2$s, מובנה ישירות בדפדפן. באמצעות הכוח של %1$s מאת Mozilla, אנו עוזרים לך להימנע מסקירות מוטות ולא אותנטיות. מודל הבינה המלאכותית שלנו משתפר תמיד כדי להגן עליך בזמן הקנייה. %2$s @@ -2296,4 +2298,6 @@ לקרוא את המאמר לפתוח את הקישור לקבלת מידע נוסף + + %s, כותרת diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 8b902f75cb..635cd63142 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -533,6 +533,11 @@ 許可 + + %1$s が Cookie を拒否しました + + このサイトに集中でき、Cookie による追跡も減少します。 + セキュリティ強化のため、自動的に HTTPS 暗号化プロトコルを使用してサイトへの接続を試行します。 @@ -643,6 +648,8 @@ アドオン + + ファイルからアドオンをインストール 通知 @@ -2271,8 +2278,6 @@ %s による広告 - レビュー チェッカーは %s の提供です。 - レビュー チェッカーは %s の提供です %s by Mozilla @@ -2335,6 +2340,8 @@ 購入する前に、%1$s で製品レビューの信頼性を確認してください。 %2$s の実験的な機能であるレビュー チェッカーはブラウザーに直接組み込まれています。 %3$s と %4$s でも動作します。 + + 購入する前に、%1$s で製品レビューの信頼性を確認してください。 %2$s の実験的な機能であるレビュー チェッカーはブラウザーに直接組み込まれています。 %1$s by Mozilla の機能を利用して偏ったレビューや偽物のレビューを回避できるように支援します。私たち AI モデルは、買い物中のお客様を保護するために常に改善されています。%2$s @@ -2402,4 +2409,6 @@ 記事を読む リンクを開いて詳細を表示 + + %s、見出し diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml index a964480ac5..d89b9eb642 100644 --- a/app/src/main/res/values-ka/strings.xml +++ b/app/src/main/res/values-ka/strings.xml @@ -519,6 +519,11 @@ ნებართვა + + %1$s ახლახან დაგეხმარათ ფუნთუშების არიდებაში + + ნაკლები ხელის შემშლელი და მეთვალყურე შიგთავსი საიტზე. + თავადვე შეეცდება დაუკავშირდეს საიტებს დაშიფრული HTTPS-ოქმით მეტი უსაფრთხოებისთვის. @@ -628,6 +633,8 @@ დამატებები + + დამატების ჩადგმა ფაილიდან შეტყობინებები @@ -2241,8 +2248,6 @@ რეკლამა – %s - მიმოხილვის შემმოწმებლის უზრუნველმყოფია %s. - მიმოხილვის შემმოწმებლის უზრუნველმყოფია %s %s Mozilla-სგან @@ -2303,8 +2308,10 @@ გამოცადეთ ჩვენი საიმედო მეგზური საყიდლების მიმოხილვებისთვის გადაამოწმეთ შეძენამდე, თუ რამდენად სანდოა გასაყიდი ნაწარმის მიმოხილვები საიტზე %1$s. მიმოხილვის შემმოწმებელი საცდელი შესაძლებლობაა %2$s-სგან და პირდაპირ ბრაუზერშივეა ჩაშენებული. მხარდაჭერილია %3$s და აგრეთვე %4$s. + + გადაამოწმეთ შეძენამდე, თუ რამდენად სანდოა გასაყიდი ნაწარმის მიმოხილვები საიტზე %1$s. მიმოხილვის შემმოწმებელი საცდელი შესაძლებლობაა %2$s-სგან და პირდაპირ ბრაუზერშივეა ჩაშენებული. - Mozilla-ს %1$s-ის ძლევამოსილებით ჩვენ დაგეხმარებით მიკერძოებული და ყალბი მიმოხილვების თავიდან აცილებაში. ჩვენი AI-მოდელი მუდმივად იხვეწება საყიდლების შეძენისას თქვენს დასაცავად. %2$s + Mozilla-ს %1$s-ის ძლევამოსილებით ჩვენ დაგეხმარებით მიკერძოებული და ყალბი მიმოხილვების თავიდან აცილებაში. ჩვენი AI-მოდელი მუდმივად იხვეწება თქვენს დასაცავად საყიდლების შეძენისას. %2$s ვრცლად @@ -2353,7 +2360,7 @@ შეფუთვა და იერსახე - მეტოქეებთან შედარებით + მეტოქეებთან შედარება @@ -2370,4 +2377,6 @@ სტატიის წაკითხვა ვრცლად იხილეთ ბმულზე + + %s, სათაური diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index 022c10a21e..1fa6c7bafb 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -73,6 +73,22 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Ur ttaǧǧa ara akk later ɣef yibenk-a + + %1$s itekkes inagan n tuqqna, azray, akked yisefka n yismal web mi ara tmedleḍ akk isfuyla usligen. %2$s + + + %1$s itekkes inagan n tuqqna, azray, akked yisefka n yismal web mi ara tmedleḍ akk accaren usligen. %2$s + + Anwa i izemren ad iwali armud-iw? + Senker iccer uslig uḍfir s yiwen usiti kan. @@ -96,6 +112,11 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Issin ugar ɣef ummesten asemday mgal inagan n tuqqna + + + Sit dagi i usenker n tɣimit tamaynut. Kkes azray-ik·im, inagan n tuqqna — kullec. + + Isɛa anekcum ɣer tkamiṛat. Ddu ɣer iɣewwaṛen Android, sit ɣef tisirag, sakin senned sireg. @@ -324,6 +345,8 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Firefox izewwir deg yimdanen uqbel idrimen, yekkat ɣef tudert-ik tabaḍnit s usewḥel n yineḍfaren n gar yismal.\n\Issin ugar deg Tasertit-nneɣ tbaḍnit. + + Iminig-nneɣ tettallit yiwet n tkebbanit ur nettnadi ɣef tedrimt, tessewḥal tikebbaniyin ara ak-iḍefren deg web.\n\nIssin ugar ɣef tsertit-nneɣ n tbaḍnit. tasertit n tbaḍnit @@ -361,6 +384,9 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Mačči tura + + Ɛreḍ iwiǧit n unadi n Firefox Rnu awiǧit n Firefox @@ -399,12 +425,16 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Nadi Nadi imseddayen n unadi + + Isumar seg yimseddayen n unadi Afeggag n tansa Ismenyifen i ufeggag n tansiwin Afeggag n tansiwin - Firefox isumer + + Issin ugar ɣef Firefox Suggest Mudd tazmilt deg Google Play Asenqes n yiɣarracen n yinagan n tuqqna + + Amsewḥel n yiɣarracen n yinagan n tuqqna Senqes iɣarracen n yinagan n tuqqna @@ -461,9 +493,13 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Asmel-a ur yettusefrak ara akka tura Rmed asenqes n yiɣerracen n yinagan n tuqqna i %1$s? + + Rmed amsewḥel n yiɣerracen n yinagan n tuqqna i %1$s? Sens asenqes n yiɣerracen n yinagan n tuqqna i %1$s? + + Sens amsewḥel n yiɣerracen n yinagan n tuqqna i %1$s? %1$s ur yezmir ara ad yagi issutar n trusi n yinagan n tuqqna s wudem awurman ɣef usmel-a. Tzemreḍ ad tazneḍ assuter i tallalt n usmel-a ɣer sdat. @@ -483,6 +519,9 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Sireg + + %1$s yugi inagan n tuqqna i kečč + Ɛreḍ ad teqqneḍ s wudem awurman ɣer yismal s useqdec n uneggaf n uwgelhen HTTPS i tɣellist ɛlayen. @@ -513,6 +552,8 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Sagen aeddac n umtawi Amiḍan Firefox/Aqeddac n umtawi ittwasnifel, Ffeɣ seg usnas akken ad iddu usnifel… + + Aqeddac i umtawi neɣ amiḍan n Mozilla yettwasenfel, Ffeɣ seg usnas akken ad iddu usnifel… Amiḍan @@ -562,9 +603,16 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Tacaṛt tawurmant n URLs + + Isumar sɣur imendaden + + Mudd tallalt i %1$s s uskan n yisumar i d-yettuwellhen sya ɣer da Isumar n %1$s + + Awi isumar seg web yeqqnen ɣer unadi-k·m Ldi iseɣwan deg isnasen @@ -663,6 +711,8 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Izegrar imaynuten llan tura + + Snirem ugar n 100 yisiɣzaf imaynuten ara ak·akem-yeǧǧen ad tsagneḍ Firefox. Snirem izegrar @@ -685,6 +735,8 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Sefrek amiḍan + + Senfel awal-ik·im uffir, sefrek alqaḍ n yisefka neɣ kkes amiḍan-ik·im Mtawi tura @@ -2166,6 +2218,10 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Tamuɣli s wazal-is seg yilɣa imaynuten Amek ara nettguccul alɣu n tɣara + + Nettwali iwellihen-a ttwamanent. + + Nettwali iwellihen sdukklen iwellihen inaflasen d yiwellihen arinaflasen. Issin ugar ɣef %s. @@ -2181,7 +2237,7 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Adellel sɣur %s - Amsenqad n tamawt yella-d s lmendad n %s. + Amsenqad n tamawt yella-d s lmendad n %s %s s Mozilla @@ -2192,6 +2248,10 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Ulac ddeqs n tamiwin akka tura Afaris ulac-it + + Mmel tuɣalin n ufaris deg tawsa + + Mmel tuɣalin n ufaris deg tawsa Adenqed n tɣara n yilɣa @@ -2200,22 +2260,42 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Aya yezmer ad yeṭṭef 60 tsinin. Tanemmirt ɣef tuzna n uneqqis! + + Ur nezmir ara ad nsenqed tamawin-a Talɣut i d-iteddun + + Aslaḍ yezga Awi-t + + Ulac talɣut i yellan akka tura + + Ulac tuqqna ɣer uzeṭṭa Senqed tuqqna ɣer uzeṭṭa syen ɛreḍ asali n usebter i tikkelt niḍen. + + Ulac talɣut ɣef yilɣa-a akka ar tura + + Senqed tɣara n yilɣa + + Ɛreḍ amnir-nneɣ yettwamanen i timawin n ufaris Issin ugar Tasertit n tbaḍnit + + Tasertit n tbaḍnit tiwtilin n useqdec + + Tiwtilin n useqdec Ih, ɛreḍ tikkelt niḍen Mačči tura + + Senqed ma ad tamneḍ tamawin ɣef ufaris-a — send ad t-taɣeḍ. Ɛreḍ amsenqad n tamawt @@ -2224,6 +2304,10 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Beta Ldi amsenqad n tamawt + + Mdel amsenqad n teskant + + %1$s n yitran seg 5 Sken drus @@ -2235,6 +2319,8 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Tanemzagt + + Akemmus d urwas Amḥizwer diff --git a/app/src/main/res/values-kk/strings.xml b/app/src/main/res/values-kk/strings.xml index 9b59db5106..90113286c9 100644 --- a/app/src/main/res/values-kk/strings.xml +++ b/app/src/main/res/values-kk/strings.xml @@ -523,6 +523,11 @@ Рұқсат ету + + %1$s сіз үшін жаңа ғана cookie файлдарынан бас тартты + + Бұл сайтта алаңдататын нәрселерді мен сізді бақылайтын cookie файлдарын азырақ қылу. + Қауіпсіздікті арттыру үшін сайттарға HTTPS шифрлеу хаттамасын пайдаланып автоматты түрде қосылу әрекетін жасайды. @@ -631,6 +636,8 @@ Қосымшалар + + Файлдан қосымшаны орнату Хабарламалар @@ -2239,8 +2246,6 @@ %s ұсынған жарнама - Пікірлерді тексеру құралы %s негізінде жасалған. - Пікірлерді тексеру құралы %s негізінде жасалған Mozilla ұсынған %s @@ -2300,6 +2305,8 @@ Өнім пікірлерінің сенімділігі туралы біздің нұсқаулықты қолданып көріңіз Сатып алғанға дейін %1$s сайтындағы өнім пікірлері қаншалықты сенімді екенін қараңыз. Пікірлерді тексеру құралын %2$s сынамалы түрде ұсынады, ол тура браузер ішінде қолжетімді — сонымен қатар, ол %3$s және %4$s сайттарында да істейді. + + Сатып алғанға дейін %1$s сайтындағы өнім пікірлері қаншалықты сенімді екенін қараңыз. Пікірлерді тексеру құралын %2$s сынамалы түрде ұсынады, ол тура браузер ішінде қолжетімді. Mozilla ұсынған %1$s мүмкіндігін пайдалана отырып, біз сізге біржақты және шынайы емес пікірлерден аулақ болуға көмектесеміз. Сатып алу кезінде сізді қорғау үшін біздің AI моделіміз үнемі жетілдіріліп отырады. %2$s @@ -2368,4 +2375,6 @@ мақаланы оқу көбірек білу үшін сілтемені ашыңыз + + %s, Тақырыптама diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 393dfef29d..cc1ee482df 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -533,6 +533,11 @@ 허용 + + %1$s가 쿠키를 거부했습니다 + + 방해 요소가 줄어들고, 이 사이트에서 사용자를 추적하는 쿠키가 줄어듭니다. + 보안 강화를 위해 HTTPS 암호화 프로토콜을 사용하여 사이트에 자동으로 연결을 시도합니다. @@ -643,6 +648,8 @@ 부가 기능 + + 파일에서 부가 기능 설치 알림 @@ -2294,8 +2301,6 @@ %s의 광고 - 리뷰 검사기는 %s에 의해 제공됩니다. - 리뷰 검사기는 %s에 의해 제공됨 Mozilla의 %s @@ -2355,6 +2360,8 @@ 제품 리뷰에 대한 신뢰할 수 있는 가이드를 사용해 보세요 구매하기 전에 %1$s에 대한 제품 리뷰가 얼마나 신뢰할 수 있는지 확인하세요. %2$s의 실험 기능인 리뷰 검사기가 브라우저에 바로 내장되어 있습니다. %3$s 및 %4$s에서도 작동합니다. + + 구매하기 전에 %1$s에 대한 제품 리뷰가 얼마나 신뢰할 수 있는지 확인하세요. %2$s의 실험 기능인 리뷰 검사기가 브라우저에 바로 내장되어 있습니다. Mozilla의 %1$s 기능을 사용하여 편향되고 허위 리뷰를 방지하는 데 도움을 드립니다. AI 모델은 쇼핑하는 동안 사용자를 보호하기 위해 항상 개선되고 있습니다. %2$s @@ -2422,4 +2429,6 @@ 글 읽기 더 알아보려면 링크 열기 + + %s, 제목 diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 37b0db0dad..2b4310ddd7 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -378,9 +378,15 @@ Ikke nå + + Prøv Firefox-søkewidgeten Med Firefox på startskjermen din har du enkel tilgang til den personvernfokuserte nettleseren som blokkerer sporing på tvers av nettsteder. + + Legg til Firefox-widget Ikke nå @@ -487,15 +493,23 @@ Nettstedet støttes for øyeblikket ikke Vil du slå på reduksjon av infokapselbannere for %1$s? + + Vil du slå på blokkering av infokapselbanner for %1$s? Vil du slå av reduksjon av infokapselbannere for %1$s? + + Vil du slå av blokkering av infokapselbanner for %1$s? %1$s kan ikke automatisk avvise forespørsler om infokapsler på dette nettstedet. Du kan sende en forespørsel om å støtte dette nettstedet i fremtiden. %1$s vill slette infokapsler og oppdatere siden. Sletting av alle infokapsler kan føre til at du blir logget ut eller at handlekurver blir tømt. + + Slå av og %1$s sletter infokapsler og laster inn dette nettstedet på nytt. Dette kan logge deg ut eller tømme handlekurver. %1$s prøver å automatisk avvise alle infokapselforespørsler på støttede nettsteder. + + Slå på, og %1$s vil prøve å automatisk nekte infokapselbannere på dette nettstedet. Tillat at %1$s avviser infokapselbannere? @@ -508,6 +522,11 @@ Tillat + + %1$s nektet nettopp infokapsler for deg + + Mindre distraksjoner, mindre infokapsler som sporer deg på denne siden. + Forsøker automatisk å koble til nettsteder ved hjelp av HTTPS-krypteringsprotokollen for økt sikkerhet. @@ -538,6 +557,8 @@ Selvvalgt synkroniseringsserver Firefox-konto/synkroniseringsserver endret. Avslutter applikasjonen for å legge til endringer… + + Mozilla-konto/synkroniseringsserver endret. Avslutter applikasjonen for å legge til endringer… Konto @@ -589,6 +610,14 @@ Autofullfør nettadresser Forslag fra sponsorer + + Støtt %1$s med sporadiske sponsede forslag + + Forslag fra %1$s + + Få forslag fra nettet relatert til søket ditt Åpne lenker i apper @@ -607,6 +636,8 @@ Utvidelser + + Installer utvidelse fra fil Varslinger @@ -681,6 +712,14 @@ Utforsk flere bakgrunnsbilder + + + Nye utvidelser nå tilgjengelig + + Sjekk ut 100+ nye utvidelser som lar deg gjøre Firefox til din egen. + + Utforsk utvidelser + Tillegget støttes ikke @@ -700,6 +739,8 @@ Behandle konto + + Endre passordet ditt, behandle datainnsamling eller slett kontoen din Synkroniser nå @@ -1374,6 +1415,8 @@ Privat fane lukket Private faner lukket + + Private nettleserdata slettet ANGRE @@ -2153,10 +2196,145 @@ Gå til innstillinger + + + Vurderingskontrollør + + Vurderingskontrollør + + Pålitelige vurderinger Blanding av pålitelige og upålitelige vurderinger Upålitelige vurderinger + + Hvor pålitelige er disse vurderingene? + + Justert vurdering + + Upålitelige vuderinger er fjernet + + Høydepunkter fra nylige vurderinger + + Hvordan vi avgjør vurderingskvalitet + + Vi bruker kunstig intelligens (AI) fra %s fra Mozilla for å sjekke påliteligheten til produktvurderinger. Dette vil bare hjelpe deg med å bedømme kvaliteten av vurderinger, ikke kvaliteten på selve produktetet. + + bokstavkarakter fra A til F.]]> + + Pålitelige vurderinger. Vi tror at vurderingene sannsynligvis kommer fra ekte kunder som har gitt ærlige, objektive vurderinger. + + Vi mener vurderingene er pålitelige. + + Vi tror det er en blanding av pålitelige og upålitelige vurderinger. + + Upålitelige vurderinger. Vi mener vurderingene sannsynligvis er falske eller fra partiske vurderere. + + Vi mener vurderingene er upålitelige. + + justerte vurderingen er kun basert på vurderinger vi mener er pålitelige.]]> + + Høydepunkter er fra %s-vurderinger i løpet av de siste 80 dagene som vi mener er pålitelige.]]> + + Les mer om %s. + + hvordan %s fra Mozilla bestemmer vurderingskvalitet + + hvordan %s bestemmer vurderingskvalitet + + Innstillinger + + Vis annonser i vurderingskontrolløren + + Du ser sporadiske annonser for relevante produkter. Alle annonser må oppfylle kvalitetsstandardene våre for vurderinger. %s + + Du ser sporadiske annonser for relevante produkter. Vi annonserer kun produkter med pålitelige vurderinger. %s + + Les mer + + Slå av vurderingskontrolløren + + Mer å vurdere + + Annonse fra %s + + Vurderingskontrolløren er drevet av%s + + %s fra Mozilla + + Ny info å sjekke + + Sjekk nå + + Ikke nok vurderinger ennå + + Når dette produktet har flere vurderinger, kan vi sjekke kvaliteten. + + Produktet er ikke tilgjengelig + + Hvis du ser at dette produktet er tilbake på lager, rapporter det og vi vil jobbe med å sjekke vurderingene. + + Rapporter at dette produktet er tilbake på lager + + Rapporter at produktet er på lager + + Kontrollerer kvaliteten på vurderingen + + Kontrollerer kvaliteten på vurderingen + + Dette kan ta cirka 60 sekunder. + + Takk for at du rapporterte! + + Vi bør ha informasjon om dette produktets anmeldelser innen 24 timer. Sjekk igjen senere. + + Vi kan ikke sjekke disse vurderingene + + Dessverre kan vi ikke sjekke vurderingskvaliteten for visse typer produkter. For eksempel gavekort og strømming av video, musikk og spill. + + Info kommer snart + + Vi bør ha informasjon om dette produktets anmeldelser innen 24 timer. Sjekk igjen senere. + + Analysen er oppdatert + + Jeg forstår + + Ingen informasjon tilgjengelig akkurat nå + + Vi jobber med å løse problemet. Prøv på nytt, snart. + + Ingen nettverkstilkobling + + Kontroller nettverkstilkoblingen og prøv å laste inn siden på nytt. + + Ingen informasjon om disse vurderingene ennå + + For å vite om dette produktets vurderinger er pålitelige, sjekk kvaliteten på vurderingen. Det tar bare omtrent 60 sekunder. + + Kontrollerer kvaliteten på vurderingen + + Prøv vår pålitelige guide til produktvurderinger + + Se hvor pålitelige produktvurderinger er på %1$s før du handler. Vurderingskontrollør, en eksperimentell funksjon fra %2$s, er innebygd rett i nettleseren. Det fungerer på %3$s og %4$s også. + + Se hvor pålitelige produktvurderinger er på %1$s før du handler. Vurderingskontrollør, en eksperimentell funksjon fra %2$s, er innebygd rett i nettleseren. + + Ved hjelp av %1$s fra Mozilla gjør vi det lettere for deg å unngå partiske og uekte anmeldelser. AI-modellen vår blir alltid bedre for å beskytte deg mens du handler. %2$s + + Les mer + + Ved å velge «Ja, prøv det» godtar du %1$s fra Mozilla sine %2$s og %3$s. + + Ved å velge «Ja, prøv det» godtar du følgende fra %1$s: + + personvernbestemmelser + + Personvernbestemmelser + + vilkår for bruk + + Vilkår for bruk Ja, prøv det @@ -2166,15 +2344,49 @@ Prøv vurderingskontrollør + + Er disse vurderingene pålitelige? Sjekk nå for å se en justert vurdering. + + Åpne vurderingskontrollør + + + Beta + + Åpne vurderingskontrolløren + + Lukk vurderingskontrolløren + + %1$s av 5 stjerner + + Vis mindre + + Vis mer + + Kvalitet + + Pris + + Frakt + + Emballasje og utseende + + Konkurranseevne + slå sammen + + sammenslått fold ut + + utvidet åpne lenken for å lære mer om denne samlingen les artikkelen åpne lenken for å lese mer + + %s, Overskrift diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 7981f681e7..2c30ffaeb3 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -467,16 +467,20 @@ Alleen-HTTPS-modus - Reductie van cookiebanners + Reductie van cookiebanners + + Blokkering van cookiebanners + + Blokkering van cookiebanners tijdens privénavigatie - Cookiebanners reduceren + Cookiebanners reduceren - Uit + Uit - Aan + Aan - %1$s probeert automatisch cookieverzoeken op cookiebanners te weigeren. + %1$s probeert automatisch cookieverzoeken op cookiebanners te weigeren. Uit voor deze website @@ -494,16 +498,24 @@ Website wordt momenteel niet ondersteund - Reductie van cookiebanners inschakelen voor %1$s? + Reductie van cookiebanners inschakelen voor %1$s? + + Blokkering van cookiebanners inschakelen voor %1$s? + + Reductie van cookiebanners uitschakelen voor %1$s? - Reductie van cookiebanners uitschakelen voor %1$s? + Blokkering van cookiebanners uitschakelen voor %1$s? %1$s kan cookieverzoeken op deze website niet automatisch weigeren. U kunt een aanvraag sturen om deze website in de toekomst te ondersteunen. - %1$s wist de cookies voor deze website en vernieuwt de pagina. Als alle cookies worden gewist, wordt u mogelijk afgemeld of worden winkelwagentjes geleegd. + %1$s wist de cookies voor deze website en vernieuwt de pagina. Als alle cookies worden gewist, wordt u mogelijk afgemeld of worden winkelwagentjes geleegd. + + Schakel dit uit en %1$s zal cookies wissen en deze website opnieuw laden. Dit kan u afmelden of winkelwagentjes legen. - %1$s probeert alle cookieverzoeken op ondersteunde websites automatisch te weigeren. + %1$s probeert alle cookieverzoeken op ondersteunde websites automatisch te weigeren. + + Schakel dit in en %1$s zal proberen alle cookiebanners op deze website automatisch te weigeren. %1$s toestaan om cookiebanners te weigeren? @@ -516,6 +528,11 @@ Toestaan + + %1$s heeft zojuist cookies voor u geweigerd + + Minder afleiding, minder cookies die u volgen op deze website. + Probeert voor een betere beveiliging automatisch middels het HTTPS-versleutelingsprotocol verbinding te maken met websites. @@ -626,6 +643,8 @@ Add-ons + + Add-on installeren via bestand Notificaties @@ -2239,8 +2258,6 @@ Advertentie van %s - Beoordelingscontrole wordt mogelijk gemaakt door %s. - Beoordelingscontrole is mogelijk gemaakt door %s %s door Mozilla @@ -2300,6 +2317,8 @@ Probeer onze vertrouwde gids voor productbeoordelingen Bekijk hoe betrouwbaar de productbeoordelingen op %1$s zijn voordat u koopt. Beoordelingscontrole, een experimentele functie van %2$s, is rechtstreeks in de browser ingebouwd. Het werkt ook op %3$s en %4$s. + + Bekijk hoe betrouwbaar de productbeoordelingen op %1$s zijn voordat u koopt. Beoordelingscontrole, een experimentele functie van %2$s, is rechtstreeks in de browser ingebouwd. Met behulp van de kracht van %1$s door Mozilla helpen we u bevooroordeelde en niet-authentieke beoordelingen te voorkomen. Ons AI-model wordt voortdurend verbeterd om u te beschermen terwijl u winkelt. %2$s @@ -2368,4 +2387,6 @@ het artikel te lezen de koppeling te openen voor meer info + + %s, koptekst diff --git a/app/src/main/res/values-nn-rNO/strings.xml b/app/src/main/res/values-nn-rNO/strings.xml index ec5baaa8dc..949d7120e1 100644 --- a/app/src/main/res/values-nn-rNO/strings.xml +++ b/app/src/main/res/values-nn-rNO/strings.xml @@ -77,6 +77,11 @@ The first parameter is the name of the app defined in app_name (for example: Firefox Nightly) The second parameter is the clickable link text in felt_privacy_info_card_subtitle_link_text --> %1$s slettar alle infokaspslar, historikk og nettstaddata når du lèt att alle private vindauge. %2$s + + %1$s slettar infokapslar, historikk og nettstad-data når du lèt att alle dei private fanene dine. %2$s @@ -207,6 +212,8 @@ Bibliotek Datamaskinversjon + + Opne i vanlig fane Legg til på startskjermen @@ -263,7 +270,7 @@ Skann - Søkjemotor + Søkjemotor Innstillingar for søkjemotor @@ -438,15 +445,15 @@ Berre HTTPS-modus - Redusering av infokapselbanner + Redusering av infokapselbanner - Reduser infokapselbanner + Reduser infokapselbanner - Av + Av - + - %1$s prøver automatisk å avvise infokapselførespurnadar på infokapselbanner. + %1$s prøver automatisk å avvise infokapselførespurnadar på infokapselbanner. Av for denne nettstaden @@ -465,26 +472,26 @@ Nettstaden er for augneblinken ikkje støtta - Vill du aktivere reduksjon av infokapselbanner for %1$s? + Vill du aktivere reduksjon av infokapselbanner for %1$s? - Vil du deaktivere reduksjon av infokapselbanner for %1$s? + Vil du deaktivere reduksjon av infokapselbanner for %1$s? %1$s kan ikkje automatisk avvise førespurnadar om infokapslar på denne nettstaden. Du kan sende ein førespurnad om å støtte denne nettstaden i framtida. - %1$s vill slette infokapslar og oppdatere sida. Sletting av alle infokapslar kan føre til at du blir logga ut eller at handlekorger vert tømde. + %1$s vill slette infokapslar og oppdatere sida. Sletting av alle infokapslar kan føre til at du blir logga ut eller at handlekorger vert tømde. - %1$s prøver å automatisk avvise alle infokapselførespurnadar på støtta nettstadar. + %1$s prøver å automatisk avvise alle infokapselførespurnadar på støtta nettstadar. - Tilate %1$s å avvise infokapselbanner? + Tilate %1$s å avvise infokapselbanner? - %1$s kan automatisk avvise fleire førespurnadar om infokapselbanner. + %1$s kan automatisk avvise fleire førespurnadar om infokapselbanner. - Ikkje no + Ikkje no - Du vil sjå færre førespurnadar om infokapslar + Du vil sjå færre førespurnadar om infokapslar - Tillat + Tillat Prøver automatisk å kople til nettstadar ved hjelp av HTTPS-krypteringsprotokollen for auka sikkerheit. @@ -2195,7 +2202,7 @@ Les meir om %s. - korleis %s frå Mozilla bestemmer kvaliteten på ei vurdering + korleis %s frå Mozilla bestemmer kvaliteten på ei vurdering Innstillingar @@ -2279,11 +2286,15 @@ Les meir - Ved å velje «Ja, prøv det» seier du deg samd i %1$s av Mozillas %2$s og %3$s. + Ved å velje «Ja, prøv det» seier du deg samd i %1$s av Mozillas %2$s og %3$s. + + personvernpraksis - personvernpraksis + Personvernerklæring + + brukarvilkår - brukarvilkår + Brukarvilkår Ja, prøv det @@ -2322,8 +2333,12 @@ slå saman + + samanslått fald ut + + utvida opne lenka for å lære meir om denne samlinga diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index 0c6c9026eb..11a222d141 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -369,6 +369,9 @@ Enviatz los onglets d’un aparelh a l’autre, gerissètz los telecargaments e obtenètz de conselhs per ne far encara mai amb Firefox. + + Enviatz d’onglets d’un aparelh a l’autre en tot seguretat e descobrissètz d’autras foncionalitats de proteccion de la vida privada de Firefox. Activar las notificacions @@ -458,16 +461,20 @@ Mòde HTTPS solament - Reduccion de las bandièras de cookies + Reduccion de las bandièras de cookies + + Blocador de bandièras de cookies + + Blocador de bandièras de cookies en navegacion privada - Reduire las bandièras de cookies + Reduire las bandièras de cookies - Desactivada + Desactivada - Activada + Activada - %1$s ensaja de regetar automaticament las demandas de cookies de las banièras de cookies. + %1$s ensaja de regetar automaticament las demandas de cookies de las banièras de cookies. Desactivada per aqueste site @@ -485,17 +492,25 @@ Site actualament pas pres en carga - Activar la reduccion de las bandièras de cookies per %1$s ? + Activar la reduccion de las bandièras de cookies per %1$s ? + + Activar lo blocador de bandièras de cookies per aqueste site %1$s ? - Desactivar la reduccion de las bandièras de cookies per %1$s ? + Desactivar la reduccion de las bandièras de cookies per %1$s ? + + Desactivar lo blocador de bandièras de cookies per %1$s ? %1$s pòt pas refusar automaticament las demandas de cookies per aqueste site. Podètz enviar una demanda de presa en carga d’aqueste site pel futur. - %1$s escafarà los cookies d’aqueste site e actualizarà la pagina. La supression de totes los cookies pòt vos desconnectar o voidar los panièrs de crompa. + %1$s escafarà los cookies d’aqueste site e actualizarà la pagina. La supression de totes los cookies pòt vos desconnectar o voidar los panièrs de crompa. + + Desactivatz-lo e %1$s escafarà los cookies puèi recargarà aqueste site. Vos poiriá desconnectar de la session o voidar lo panièr de crompas. - %1$s ensaja de regetar automaticament totas las demandas de cookies suls sites compatibles. + %1$s ensaja de regetar automaticament totas las demandas de cookies suls sites compatibles. + + Activatz-lo e %1$s ensajarà de refusar automaticament las banièras de cookies sus aqueste site. Autorizar %1$s a refusar las bandièras de cookies ? @@ -509,6 +524,12 @@ Autorizar + + %1$s ven de refusar un cookie per vos + + + Mens de distraccions, mens de cookies que vos pistan sus aqueste site. + Ensaja automaticament de se connectar als sites amb lo chiframent HTTPS per una seguretat melhorada. @@ -539,6 +560,8 @@ Servidor Sync personalizat Servidor de compte o de sincronozacion modificat. L’aplicacion se tanca per aplicar las modificacions… + + Servidor de sincronizacion o compte Mozilla modificat. Tampadura de l’aplicacion per aplicar las modificacions… Compte @@ -616,6 +639,8 @@ Moduls complementaris + + Installar un modul d’un fichièr estant Notificacions @@ -2229,8 +2254,6 @@ Exemple :\nhttps://suggestqueries.google.com/complete/search?client=firefox& Publicitat de %s - Lo verificador d’avises fonciona gràcia a %s. - Lo verificador d’avises fonciona gràcia a %s %s per Mozilla @@ -2303,4 +2326,6 @@ Exemple :\nhttps://suggestqueries.google.com/complete/search?client=firefox& legir l’article dobrissètz lo ligam per ne saber mai + + %s, títol diff --git a/app/src/main/res/values-pa-rIN/strings.xml b/app/src/main/res/values-pa-rIN/strings.xml index 3600a63073..ec3c9216e0 100644 --- a/app/src/main/res/values-pa-rIN/strings.xml +++ b/app/src/main/res/values-pa-rIN/strings.xml @@ -532,6 +532,11 @@ ਮਨਜ਼ੂਰ + + %1$s ਨੇ ਹੁਣੇ ਤੁਹਾਡੇ ਲਈ ਕੂਕੀਜ਼ ਤੋਂ ਇਨਕਾਰ ਕੀਤਾ ਹੈ + + ਇਸ ਸਾਈਟ ਲਈ ਤੁਹਾਡੇ ਲਈ ਘੱਟ ਧਿਆਨ ਭਟਕਣਾ, ਘੱਟ ਕੂਕੀਜ਼ ਟਰੈਕਿੰਗ ਹੈ। + ਵਾਧਾ ਕੀਤੀ ਸੁਰੱਖਿਆ ਲਈ HTTPS ਇੰਕ੍ਰਿਪਸ਼ਨ ਪਰੋਟੋਕਾਲ ਵਰਤ ਕੇ ਸਾਈਟਾਂ ਨਾਲ ਕਨੈਕਟ ਕਰਨ ਦੀ ਆਪਣੇ-ਆਪ ਕੋਸ਼ਿਸ਼ ਕਰੋ। @@ -643,6 +648,8 @@ ਐਡ-ਆਨ + + ਫਾਈਲ ਤੋਂ ਐਡ-ਆਨ ਇੰਸਟਾਲ ਕਰੋ ਨੋਟੀਫਿਕੇਸ਼ਨ @@ -2262,8 +2269,6 @@ %s ਵਲੋਂ ਇਸ਼ਤਿਹਾਰ - ਰੀਵਿਊ ਚੈਕਰ %s ਰਾਹੀਂ ਚੱਲਦਾ ਹੈ - ਰੀਵਿਊ ਚੈਕਰ %s ਰਾਹੀਂ ਚੱਲਦਾ ਹੈ Mozilla ਵਲੋਂ %s @@ -2323,6 +2328,8 @@ ਉਤਪਾਦ ਰੀਵਿਊ ਲਈ ਸਾਡੀ ਭਰੋਸੇਯੋਗ ਗਾਈਡ ਨੂੰ ਅਜ਼ਮਾਓ ਖਰੀਦਣ ਤੋਂ ਪਹਿਲਾਂ ਵੇਖੋ ਕਿ %1$s ਉੱਤੇ ਉਤਪਾਦ ਰੀਵਿਊ ਕਿੰਨੇ ਭਰੋਸੇਯੋਗ ਹਨ। ਰੀਵਿਊ ਚੈਕਰ, %2$s ਵਲੋਂ ਹਾਲੇ ਤਜਰਬੇ ਅਧੀਨ ਫ਼ੀਚਰ ਹੈ, ਜੋ ਤੁਹਾਡੇ ਬਰਾਊਜ਼ਰ ਵਿੱਚ ਮੌਜੂਦ ਹੈ। ਇਹ %3$s ਅਤੇ %4$s ਨਾਲ ਵੀ ਕੰਮ ਕਰਦਾ ਹੈ। + + ਖਰੀਦਣ ਤੋਂ ਪਹਿਲਾਂ ਵੇਖੋ ਕਿ %1$s ਉੱਤੇ ਉਤਪਾਦ ਰੀਵਿਊ ਕਿੰਨੇ ਭਰੋਸੇਯੋਗ ਹਨ। ਰੀਵਿਊ ਚੈਕਰ, %2$s ਵਲੋਂ ਹਾਲੇ ਤਜਰਬੇ ਅਧੀਨ ਫ਼ੀਚਰ ਹੈ, ਜੋ ਤੁਹਾਡੇ ਬਰਾਊਜ਼ਰ ਵਿੱਚ ਮੌਜੂਦ ਹੈ। Mozilla ਵਲੋਂ %1$s ਦੇ ਰਾਹੀਂ ਅਸੀਂ ਤੁਹਾਨੂੰ ਪੱਖਪਾਤੀ ਅਤੇ ਗ਼ੈਰ-ਪ੍ਰਮਾਣਿਤ ਰੀਵਿਊਆਂ ਤੋਂ ਬਚਾਅ ਸਕਦੇ ਹਾਂ। ਸਾਡਾ AI ਮਾਡਲ ਤੁਹਾਨੂੰ ਖਰੀਦਦਾਰੀ ਕਰਨ ਦੌਰਾਨ ਸੁਰੱਖਿਅਤ ਕਰਨ ਲਈ ਹਮੇਸ਼ਾਂ ਸੁਧਾਰ ਕਰ ਰਿਹਾ ਹੈ। %2$s @@ -2390,4 +2397,6 @@ ਲੇਖ ਨੂੰ ਪੜ੍ਹੋ ਹੋਰ ਜਾਣਨ ਲਈ ਲਿੰਕ ਨੂੰ ਖੋਲ੍ਹੋ + + %s, ਹੈਡਿੰਗ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 6ce7df57bf..b431879c76 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -524,6 +524,11 @@ Pozwól + + %1$s właśnie odrzucił ciasteczka za Ciebie + + Mniej odwracania uwagi, mniej ciasteczek śledzących Cię na tej witrynie. + Automatycznie próbuje łączyć się ze stronami za pomocą protokołu szyfrowania HTTPS w celu zwiększenia bezpieczeństwa. @@ -634,6 +639,8 @@ Dodatki + + Zainstaluj dodatek z pliku Powiadomienia @@ -2201,4 +2208,6 @@ przeczytać artykuł otworzyć odnośnik z informacjami + + %s, nagłówek diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 96946fa9bb..665b4bcb65 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -526,6 +526,11 @@ Permitir + + O %1$s acabou de recusar cookies para você + + Menos distrações, menos cookies rastreando você neste site. + Tentar se conectar com sites usando automaticamente o protocolo de criptografia HTTPS para maior segurança. @@ -635,6 +640,8 @@ Extensões + + Instalar extensão a partir de arquivo Notificações @@ -2251,8 +2258,6 @@ Anúncio de %s - O verificador de avaliações tem tecnologia %s. - O verificador de avaliações tem tecnologia %s %s da Mozilla @@ -2314,6 +2319,8 @@ Veja a confiabilidade das avaliações de um produto em %1$s antes de comprar. O verificador de avaliações, um recurso experimental do %2$s, é integrado no navegador. Também funciona em %3$s e %4$s. + + Veja a confiabilidade das avaliações de um produto em %1$s antes de comprar. O verificador de avaliações, um recurso experimental do %2$s, é integrado no navegador. Usando o poder do %1$s da Mozilla, ajudamos você a evitar avaliações tendenciosas e não autênticas. Nosso modelo de inteligência artificial está sempre melhorando para te proteger enquanto faz compras. %2$s @@ -2381,4 +2388,6 @@ ler o artigo abrir o link para saber mais + + %s, título de seção diff --git a/app/src/main/res/values-rm/strings.xml b/app/src/main/res/values-rm/strings.xml index a272f481db..3f867503b0 100644 --- a/app/src/main/res/values-rm/strings.xml +++ b/app/src/main/res/values-rm/strings.xml @@ -516,6 +516,11 @@ Permetter + + %1$s ha refusà cookies per tai + + Damain distracziun, damain cookies che ta fastizeschan sin questa website. + Empruvar da connectar automaticamain cun websites cun agid dal protocol da criptadi HTTPS per dapli segirezza. @@ -625,6 +630,8 @@ Supplements + + Installar in supplement a basa dad ina datoteca Communicaziuns @@ -2240,8 +2247,6 @@ Reclama da %s - Il verificatur da recensiuns funcziuna grazia a %s. - Il verificatur da recensiuns funcziuna grazia a %s %s da Mozilla @@ -2301,6 +2306,8 @@ Emprova noss guid fidà per recensiuns da products Ve a savair quant fidablas che recensiuns da products èn sin %1$s avant che ti als cumpras. Il verificatur da recensiuns, ina funcziun experimentala da %2$s, è integrada directamain en il navigatur. Quai funcziuna cun %3$s ed era cun %4$s. + + Ve a savair quant fidablas che las recensiuns dals products èn sin %1$s avant che ti als cumpras. La verificaziun da recensiuns, ina funcziun experimentala da %2$s, è integrada directamain en il navigatur. Nus duvrain il potenzial da %1$s da Mozilla per ta gidar ad evitar recensiuns nunautenticas e tendenziusas. Noss model dad intelligenza artifiziala vegn meglierà permanentamain per ta proteger durant che ti fas cumpras. %2$s @@ -2367,4 +2374,6 @@ leger l\'artitgel avrir la colliaziun per vegnir a savair dapli + + %s, titel diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e777c6b220..a196d140d9 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -532,6 +532,11 @@ Разрешить + + %1$s только что отказался от куки для вас + + Меньше отвлекающих факторов, меньше куки, отслеживающих вас на этом сайте. + Автоматически пытаться подключиться к сайтам через протокол шифрования HTTPS для повышения безопасности. @@ -642,6 +647,8 @@ Дополнения + + Установить дополнение из файла Уведомления @@ -1123,7 +1130,7 @@ Изменить - Скопировать + Копировать Поделиться @@ -1346,7 +1353,7 @@ Недавно использованные - Скопировать в буфер обмена + Копировать в буфер обмена Скопировано в буфер обмена @@ -1787,11 +1794,11 @@ Имя пользователя скопировано в буфер обмена - Скопировать пароль + Копировать пароль Очистить пароль - Скопировать имя пользователя + Копировать имя пользователя Очистить имя пользователя @@ -2268,8 +2275,6 @@ Реклама от %s - Инструмент проверки отзывов разработан %s. - Инструмент проверки отзывов разработан %s %s от Mozilla @@ -2329,6 +2334,8 @@ Оцените наше руководство по достоверности отзывов на продукты Прежде чем купить, посмотрите, насколько достоверны отзывы о товаре на %1$s. Проверка отзывов — экспериментальная функция от %2$s — встроена прямо в браузер. Она также работает на %3$s и %4$s. + + Прежде чем купить, посмотрите, насколько надежны отзывы о продуктах на %1$s. Проверка отзывов, экспериментальная функция от %2$s, встроена прямо в браузер. Используя возможности %1$s от Mozilla, мы помогаем вам избежать предвзятых и недостоверных отзывов. Наша модель искусственного интеллекта постоянно совершенствуется, чтобы защитить вас во время покупок. %2$s @@ -2429,4 +2436,6 @@ Вы уверены, что хотите удалить файл кэша метаданных аддонов? Да Отмена + + %s, Заголовок diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index 5b26e3b22e..4f967fa723 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -257,7 +257,7 @@ පරිලෝකනය - සෙවුම් යන්ත්‍රය + සෙවුම් යන්ත්‍රය සෙවුම් යන්ත්‍රයේ සැකසුම් @@ -447,15 +447,17 @@ HTTPS-පමණි ප්‍රකාරය - දත්තකඩ පතාක අවකරණය + දත්තකඩ පතාක අවකරණය + + දත්තකඩ පතාක අවහිරය - දත්තකඩ පතාක අවම කරන්න + දත්තකඩ පතාක අවම කරන්න - අක්‍රිය + අක්‍රිය - සක්‍රිය + සක්‍රිය - %1$s ස්වයංක්‍රීයව දත්තකඩ පතාකවල දත්තකඩ ඉල්ලීම් ප්‍රතික්‍ෂේප කිරීමට උත්සාහ කරයි. + %1$s ස්වයංක්‍රීයව දත්තකඩ පතාකවල දත්තකඩ ඉල්ලීම් ප්‍රතික්‍ෂේප කිරීමට උත්සාහ කරයි. මෙම අඩවියට අක්‍රියයි @@ -473,27 +475,27 @@ අඩවියට සහාය නොදක්වයි - %1$s සඳහා දත්තකඩ පතාක අවකරණය සක්‍රිය කරන්නද? + %1$s සඳහා දත්තකඩ පතාක අවකරණය සක්‍රිය කරන්නද? - %1$s සඳහා දත්තකඩ පතාක අවකරණය අක්‍රිය කරන්නද? + %1$s සඳහා දත්තකඩ පතාක අවකරණය අක්‍රිය කරන්නද? %1$s මඟින් මෙම අඩවියේ දත්තකඩ ඉල්ලීම් ස්වයංක්‍රීයව ඉවතලීමට නොහැක. ඉදිරියේ දී මෙම අඩවිය සඳහා සහාය දැක්වීමට ඉල්ලීමක් යැවීමට හැකිය. - %1$s මෙම අඩවියේ දත්තකඩ ඉවත් කර පිටුව නැවුම් කරයි. සියළුම දත්තකඩ මැකීමෙන් බඩු කරත්ත හිස් වීමට හෝ ඔබව නික්මවීමට ඉඩ ඇත. + %1$s මෙම අඩවියේ දත්තකඩ ඉවත් කර පිටුව නැවුම් කරයි. සියළුම දත්තකඩ මැකීමෙන් බඩු කරත්ත හිස් වීමට හෝ ඔබව නික්මවීමට ඉඩ ඇත. - %1$s සහාය දක්වන අඩවිවල තිබෙන සියළුම දත්තකඩ ඉල්ලීම් ස්වයංක්‍රීයව ප්‍රතික්‍ෂේප කිරීමට උත්සාහ කරයි. + %1$s සහාය දක්වන අඩවිවල තිබෙන සියළුම දත්තකඩ ඉල්ලීම් ස්වයංක්‍රීයව ප්‍රතික්‍ෂේප කිරීමට උත්සාහ කරයි. - දත්තකඩ පතාක ප්‍රතික්‍ෂේපයට %1$s වෙත ඉඩ දෙන්නද? + දත්තකඩ පතාක ප්‍රතික්‍ෂේපයට %1$s වෙත ඉඩ දෙන්නද? - %1$s මඟින් බොහෝ දත්තකඩ පතාක ඉල්ලීම් ස්වයංක්‍රීයව ඉවතලිය හැකිය. + %1$s මඟින් බොහෝ දත්තකඩ පතාක ඉල්ලීම් ස්වයංක්‍රීයව ඉවතලිය හැකිය. - දැන් නොවේ + දැන් නොවේ - ඔබ දත්තකඩ ඉල්ලීම් අඩුවෙන් දකිනු ඇත + ඔබ දත්තකඩ ඉල්ලීම් අඩුවෙන් දකිනු ඇත - ඉඩ දෙන්න + ඉඩ දෙන්න ඉහළ ආරක්‍ෂාවක් සඳහා HTTPS සංකේතන කෙටුම්පත භාවිතයෙන් අඩවි වෙත ස්වයංක්‍රීයව සම්බන්ධ වීමට තැත් කරයි. @@ -2187,9 +2189,9 @@ තව දැනගන්න - පෞද්ගලිකත්‍ව ප්‍රතිපත්තිය + පෞද්ගලිකත්‍ව ප්‍රතිපත්තිය - භාවිත නියම + භාවිත නියම ඔව්, උත්සාහ කරමු diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index ecb454bcfc..017932ce11 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -530,6 +530,11 @@ Povoliť + + %1$s pre vás práve odmietol súbory cookie + + Menej rozptyľovania, menej súborov cookie, ktoré vás na tejto stránke sledujú. + Automaticky sa pokúša pripojiť k stránkam pomocou šifrovacieho protokolu HTTPS na zvýšenie bezpečnosti. @@ -640,6 +645,8 @@ Doplnky + + Nainštalovať doplnok zo súboru Upozornenia @@ -1549,7 +1556,7 @@ Prihláste sa pomocou fotoaparátu - Použiť radšej e-mailovú adresu + Použiť radšej e‑mailovú adresu Vytvorte si ho a synchronizujte svoj Firefox medzi zariadeniami.]]> @@ -1833,7 +1840,7 @@ Ukladať a automaticky dopĺňať adresy - Zahŕňa informácie ako sú čísla, e-mailové adresy a dodacie adresy + Zahŕňa informácie ako sú čísla, e‑mailové adresy a dodacie adresy Pridať kartu @@ -1913,7 +1920,7 @@ Telefón - E-mail + E‑mail Uložiť @@ -2151,7 +2158,7 @@ Vyhľadávanie %s - Nastavte si automatické otváranie webových stránok, e-mailov a správ vo Firefoxe. + Nastavte si automatické otváranie webových stránok, e‑mailov a správ vo Firefoxe. Odstrániť @@ -2249,8 +2256,6 @@ Reklama od %s - Kontrola recenzií používa technológiu %s. - Kontrola recenzií používa technológiu %s %s od Mozilly @@ -2311,6 +2316,8 @@ Vyskúšajte nášho dôveryhodného sprievodcu recenziami produktov Pred nákupom sa presvedčte, aké spoľahlivé sú recenzie produktov predajcu %1$s. Kontrola recenzií, experimentálna funkcia prehliadača %2$s, je zabudovaná priamo do prehliadača. Podporuje aj %3$s a %4$s. + + Pred nákupom sa presvedčte, aké spoľahlivé sú recenzie produktov predajcu %1$s. Kontrola recenzií, experimentálna funkcia prehliadača %2$s, je zabudovaná priamo do prehliadača. Pomocou nástroja %1$s od Mozilly vám pomôžeme vyhnúť sa neobjektívnym a neautentickým recenziám. Náš AI model sa neustále zlepšuje, aby vás chránil pri nakupovaní. %2$s @@ -2377,4 +2384,6 @@ prečítať článok otvorte odkaz a dozviete sa viac + + %s, nadpis diff --git a/app/src/main/res/values-skr/strings.xml b/app/src/main/res/values-skr/strings.xml index b6591819fb..472cd30ad6 100644 --- a/app/src/main/res/values-skr/strings.xml +++ b/app/src/main/res/values-skr/strings.xml @@ -397,17 +397,19 @@ ایچ ٹی ٹی پی ایس ــ صرف موڈ - کوکی بینر گھٹاوݨ + کوکی بینر گھٹاوݨ + + کوکی بینر بلاک کرݨ آلا - کوکی بینراں کوں تھوڑا کرو + کوکی بینراں کوں تھوڑا کرو - بند + بند - چالو + چالو - %1$s کوکی بینراں تے کوکی ارداساں کوں آپݨے آپ مسترد کرݨ دی کوشش کریندے۔ + %1$s کوکی بینراں تے کوکی ارداساں کوں آپݨے آپ مسترد کرݨ دی کوشش کریندے۔ ایں سائٹ کیتے بند کرو @@ -424,15 +426,15 @@ سائٹ فی الحال سہارا تھئی کائنی - %1$s کیتے کوکی بینر گھٹاوݨ چالو کروں؟ + %1$s کیتے کوکی بینر گھٹاوݨ چالو کروں؟ - %1$s کیتے کوکی بینر گھٹاوݨ بند کروں؟ + %1$s کیتے کوکی بینر گھٹاوݨ بند کروں؟ - %1$s ایں سائٹ دیاں کوکیاں صاف کریسی تے ورقہ تازہ کریسی۔ساریاں کوکیاں صاف کرݨ نال تساں سائن آوٹ تھی سڳدے ہو یا تہاݙی خریداری ریڑھی خالی تھی ویسی۔ + %1$s ایں سائٹ دیاں کوکیاں صاف کریسی تے ورقہ تازہ کریسی۔ساریاں کوکیاں صاف کرݨ نال تساں سائن آوٹ تھی سڳدے ہو یا تہاݙی خریداری ریڑھی خالی تھی ویسی۔ - %1$s آپݨے آپ سہارا تھیاں سائٹاں تے ساریاں کوکی ارداساں کوں مسترد کرݨ دی کوشش کریندا ہے۔ + %1$s آپݨے آپ سہارا تھیاں سائٹاں تے ساریاں کوکی ارداساں کوں مسترد کرݨ دی کوشش کریندا ہے۔ بھلا %1$s کوں کوکی بینراں مسترد کرݨ دی اجازت ݙیوں؟ @@ -2061,8 +2063,12 @@ ترتیباں تے ون٘ڄو + + %s بارے ٻیا سِکھو۔ ترتیباں + + ٻیا سِکھو سمجھ گھدے diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 7e3c4086f0..8b8c7f916b 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -504,8 +504,12 @@ %1$s bo počistil piškotke tega spletnega mesta in osvežil stran. Če počistite vse piškotke, boste morda odjavljeni ali se bo izpraznila vaša nakupovalna košarica. + + Izklopite in %1$s bo izbrisal piškotke ter znova naložil stran, kar vas lahko odjavi in izprazni vaš nakupovalni voziček. %1$s poskuša samodejno zavrniti vse zahteve za shranjevanje piškotkov na spletnih mestih, ki so podprta. + + Vklopite to možnost in %1$s bo skušal na tem spletnem mestu samodejno zavrniti pasice s piškotki. Dovolite %1$su, da zavrača pasice s piškotki? @@ -518,6 +522,11 @@ Dovoli + + %1$s je pravkar zavrnil piškotke v vašem imenu + + Manj motenj in manj piškotkov, ki vam sledijo po tem spletnem mestu. + Za večjo varnost poskuša samodejno vzpostaviti povezavo s šifrirnim protokolom HTTPS. @@ -628,6 +637,8 @@ Dodatki + + Namesti dodatek iz datoteke Obvestila @@ -2258,8 +2269,6 @@ Oglas %sa - Pregledovalnik mnenj uporablja tehnologijo %s. - Pregledovalnik mnenj uporablja tehnologijo %s Mozilla %s @@ -2319,6 +2328,8 @@ Preizkusite naš zaupanja vreden vodnik po ocenah izdelkov Pred nakupom preverite, kako zanesljiva so mnenja o izdelkih v trgovini %1$s. Pregledovalnik mnenj, preizkusna zmogljivost %2$sa, je vgrajen neposredno v brskalnik. Deluje tudi v trgovinah %3$s in %4$s. + + Pred nakupom preverite, kako zanesljiva so mnenja o izdelkih v trgovini %1$s. Pregledovalnik mnenj, preizkusna zmogljivost %2$sa, je vgrajen neposredno v brskalnik. Mozilla %1$s vam omogoča, da se izognete pristranskim in nepristnim mnenjem. Naš model umetne inteligence se nenehno izboljšuje, da vas ščiti med nakupovanjem. %2$s @@ -2386,4 +2397,6 @@ preberete članek odprete povezavo s podrobnostmi + + %s, naslov diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index 61e580158a..6b4c2dc94d 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -205,6 +205,8 @@ Bibliotekë Sajt për desktop + + Hape në skedë të rregullt Shtoje te skena e Kreut @@ -262,7 +264,7 @@ Skanoje - Motor kërkimesh + Motor kërkimesh Rregullime motorësh kërkimesh @@ -332,7 +334,7 @@ Duam fort t’ju mbajmë të parrezik - Firefox-i vë njerëzit mbi fitimet dhe mbron privatësinë tuaj duke bllokuar gjurmues nga sajti në sajt.\n\nMësoni më tepër te shënimi ynë mbi privatësinë. + Firefox-i vë njerëzit mbi fitimet dhe mbron privatësinë tuaj duke bllokuar gjurmues “nga sajti në sajt”.\n\nMësoni më tepër te shënimi ynë mbi privatësinë. Shfletuesi ynë, me entin jofitimprurës nga pas, ndihmon të ndalen shoqëri t’ju ndjekin fshehtazi nëpër internet.\n\nMësoni më tepër te shënimi ynë mbi privatësinë. Jini i fshehtëzuar, kur hidheni nga një pajisje në tjetrën - Merrni skeda dhe fjalëkalim nga pajisjet tuaja të tjera, për të vazhduar atje ku e latë. + Merrni skeda dhe fjalëkalime nga pajisjet tuaja të tjera, për të vazhduar atje ku e latë. Kur keni bërë hyrjen në llogari dhe njëkohësim, jeni më të siguruar. Firefox-i fshehtëzon fjalëkalimet tuaja, faqerojtësit, etj. @@ -457,16 +459,20 @@ Mënyra Vetëm-HTTPS - Reduktim Banderolash Për Cookie-t + Reduktim Banderolash Për Cookie-t + + Bllokues Banderolash Cookie-sh + + Bllokues Banderolash Cooki-esh në shfletim privat - Redukto banderola për cookie-t + Redukto banderola për cookie-t - Off + Off - On + On - %1$s provon vetvetiu të hedhë oshtë kërkesa depozitimi cookie -sh nga banderola cookie-sh. + %1$s provon vetvetiu të hedhë poshtë kërkesa depozitimi cookie -sh nga banderola cookie-sh. Çaktivizuar për këtë sajt @@ -485,27 +491,40 @@ Sajt aktualisht i pambuluar - Të aktivizohet Reduktim Banderolash Cookie-sh për %1$s? + Të aktivizohet Reduktim Banderolash Cookie-sh për %1$s? + + Të aktivizohet Bllokim Banderolash Cookie-sh për %1$s? + + Të çaktivizohet Reduktim Banderolash Cookie-sh për %1$s? - Të çaktivizohet Reduktim Banderolash Cookie-sh për %1$s? + Të çaktivizohet Bllokim Banderolash Cookie-sh për %1$s? - %1$s s’mund të hedhë poshtë automatikisht kërkesa për cookie në këtë sajt. Mund të dërgoni një kërkesë për mbulimin e këtij sajti në të ardhmen. + %1$s s’mund të hedhë poshtë automatikisht kërkesa për “cookie” në këtë sajt. Mund të dërgoni një kërkesë për mbulimin e këtij sajti në të ardhmen. - %1$s do të spastrojë cookie-t për këtë sajt dhe do të rifreskojë faqen. Spastrimi i krejt cookie-ve mund të sjellë nxjerrjen tuaj nga llogaria, ose zbrazje shportash blerjesh. + %1$s do të spastrojë cookie-t për këtë sajt dhe do të rifreskojë faqen. Spastrimi i krejt cookie-ve mund të sjellë nxjerrjen tuaj nga llogaria, ose zbrazje shportash blerjesh. + + Çaktivizojeni dhe %1$s do të spastrojë cookie-t dhe do të ringarkojë këtë sajt. Kjo mund sjellë nxjerrjen tuaj nga llogaria, ose zbrazjen e shportave tuaja të blerjes. + + %1$s provon të hedhë poshtë automatikisht krejt kërkesat për cookies, në sajtet që e mbulojnë. - %1$s provon të hedhë poshtë automatikisht krejt kërkesat për cookies, në sajtet që e mbulojnë. + Aktivizojeni dhe %1$s do të provojë të hedhë poshtë automatikisht banderola cookie-sh në këtë sajt. - Të lejohet %1$s të hedhë poshtë banderola cookie-sh? + Të lejohet %1$s të hedhë poshtë banderola cookie-sh? - %1$s mund të hedhë vetvetiu poshtë mjaft kërkesa banderolash cookie-sh. + %1$s mund të hedhë vetvetiu poshtë mjaft kërkesa banderolash cookie-sh. - Jo Tani + Jo Tani - Do të shihni më pak kërkesa depozitimi cookie-sh + Do të shihni më pak kërkesa depozitimi cookie-sh - Lejoje + Lejoje + + + %1$s sapo hodhi poshtë “cookies” për ju + + Më pak shpërqendrim, më pak “cookies” që ju ndjekin në këtë sajt. Përpiqet automatikisht të lidhet me sajtet duke përdorur protokollin HTTPS të fshehtëzimit, për më tepër siguri. @@ -558,7 +577,7 @@ Llogari Mozilla - Rilidhu që të rimerret njëkohësimi + Rilidhuni për të vazhduar njëkohësimin Gjuhë @@ -591,7 +610,7 @@ Sugjerime nga sponsorë - Përkrahni %1$s-in, përmes sugjerimesh të sponsorizuara, me raste + Përkrahni %1$s-in, përmes sugjerimesh, të ndonjëherëshme, të sponsorizuara Sugjerime nga %1$s @@ -617,6 +636,8 @@ Shtesa + + Instaloni shtesë prej kartele Njoftime @@ -661,7 +682,7 @@ - Sfond: %1$s + Element Sfondi: %1$s Sfondi u përditësua! @@ -708,7 +729,7 @@ Shtesat janë çaktivizuar përkohësisht - Në ose më tepër shtesa reshtën së funksionuari, duke e bërë të paqëndrueshëm sistemin tuaj. %1$s-i u rrek, pa sukses, të rinisë shtesën(at).\n\nShtesat s’do të rinisen gjatë sesionit tuaj të tanishëm.\n\nHeqja, ose çaktivizimi i shtesave mund ta ndreqë këtë problem. + Një ose më tepër shtesa reshtën së funksionuari, duke e bërë të paqëndrueshëm sistemin tuaj. %1$s-i u rrek, pa sukses, të rinisë shtesën(at).\n\nShtesat s’do të rinisen gjatë sesionit tuaj të tanishëm.\n\nHeqja, ose çaktivizimi i shtesave mund ta ndreqë këtë problem. Provoni të rinisni shtesa @@ -1182,7 +1203,7 @@ Depozitë e Qëndrueshme - Cookies nga sajti në sajt + “Cookies” palësh të treta Lëndë nën DRM @@ -1540,7 +1561,7 @@ Mbrojtje e Thelluar Nga Gjurmimi - Tashmë me Mbrojtje Totale Nga Cookie-t, barriera jonë më e fuqishme deri më sot kundër gjurmuesve nga sajti në sajt. + Tashmë me Mbrojtje Tërësore Nga Cookie-t, barriera jonë më e fuqishme deri më sot kundër gjurmuesve nga sajti në sajt. %s ju mbron nga shumë prej gjurmuesve më të rëndomtë që ndjekin ç’bëni në internet. @@ -1575,7 +1596,7 @@ Krejt cookie-t (do të shkaktojë mosfunksionim sajtesh) - Izoloni cookies nga sajti në sajt + Izoloni “cookies” palësh të treta Lëndë gjurmimi @@ -1599,7 +1620,7 @@ <em>Cookies</em> Gjurmimi Nga Sajte Në Sajte - Cookies Nga Sajti Në Sajt + “Cookies” Nga Palë të Treta Bllokon <em>cookies</em> të cilat rrjete reklamash dhe shoqëri analizimesh i përdorin për të përpiluar të dhëna tuajat të shfletimit nëpër mjaft sajte. @@ -2205,7 +2226,9 @@ Mësoni më tepër mbi %s. - si e përcakton cilësinë e shqyrtimeve %s nga Mozilla + si e përcakton cilësinë e shqyrtimeve %s nga Mozilla + + si e përcakton %s cilësinë e shqyrtimeve Rregullime @@ -2223,8 +2246,6 @@ Reklamë nga %s - Kontrollori i shqyrtimeve bazohet në %s. - Kontrollori i shqyrtimeve bazohet në %s %s nga Mozilla @@ -2284,16 +2305,24 @@ Provoni udhërrëfyesin tonë të besuar për shqyrtime produktesh Shihni se sa të besueshme janë shqyrtime në %1$s, para se të blini. Kontrollori i Shqyrtimeve, një veçori eksperimentale prej %2$s-s, është ndërtuar drejt e në shfletues. Funksionon edhe në %3$s dhe %4$s. + + Shihni se sa të besueshme janë shqyrtime produktesh në %1$s, para se të blini. Kontrollori i Shqyrtimeve, një veçori eksperimentale prej %2$s-s, është ndërtuar drejt e në shfletues. Duke përdorur fuqinë e %1$s nga Mozilla, ju ndihmojmë të shmangni shqyrtime të njëanshme dhe jo të mirëfillta. Modeli ynë AI përmirësohet përherë, për t’ju mbrojtur teksa blini në internet. %2$s Mësoni më tepër - Duke përzgjedhur “Po, Provojeni”, pajtoheni me %1$s nga %2$s dhe %3$s të Mozilla-s. + Duke përzgjedhur “Po, Provojeni”, pajtoheni me %1$s nga %2$s dhe %3$s të Mozilla-s. + + Duke përzgjedhur “Po, provojeni”, pajtoheni me sa vijon prej %1$s: - rregulla privatësie + rregulla privatësie + + Rregulla privatësie + + kushte përdorimi - kushte përdorimi + Kushte përdorimi Po, provojeni @@ -2344,4 +2373,6 @@ lexoni artikullin që të mësoni më tepër, hapni lidhjen + + %s, Krye diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 43ce6b0b06..3c217711df 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -333,6 +333,8 @@ Нека Firefox буде ваш главни прегледач + + Бринемо о вашој безбедности Firefox ставља људе испред профита и штити вашу приватност тако што блокира елементе за праћење.\n\nСазнајте више у нашем обавештењу о приватности. @@ -348,8 +350,13 @@ Брзо се пребацујте између телефона и лаптопа + + Останите заштићени шифровањем када мењате уређаје Пренесите картице и лозинке за ваших осталих уређаја и наставите тамо где сте стали. + + Безбеднији сте када се пријавите и синхронизујете. Firefox шифрује ваше лозинке, обележиваче и остало. Пријави се @@ -357,14 +364,32 @@ Обавештења вам помажу да урадите више уз Firefox + + Обавештења помажу Firefox-у да вас заштити Шаљите картице између уређаја, управљајте преузимањима и добијајте савете о томе како да најбоље искористите Firefox. + + Безбедно шаљите језичке између уређаја и откријте нове функције приватности у Firefox-у. Укључи обавештења Не сада + + Испробајте Firefox-ов виџет за претрагу + + Уз Firefox на почетном екрану, добијате лак приступ прегледачу који брине о приватности и блокира елементе за праћење. + + Додај Firefox виџет + + Не сада + Отвори нови %1$s језичак @@ -387,6 +412,8 @@ Изаберите Управљајте пречицама за претрагу + + Управљајте другим претраживачима Уредите претраживаче приказане у менију за претрагу @@ -395,8 +422,18 @@ Подразумевани претраживач Претрага + + Претраживачи + + Предлози претраживача Адресна трака + + Подешавања адресне траке + + Адресна трака - Firefox предлог + + Сазнајте више о Firefox предлогу Оцените на Google Play продавници Смањење банера колачића + + Блокатор банера колачића + + Блокатор банера колачића у приватном прегледању Смањи банере колачића @@ -454,15 +495,23 @@ Сајт тренутно није подржан Укључити смањење банера колачића за %1$s? + + Укључити блокатор банера колачића за %1$s? Икључити смањење банера колачича за %1$s? + + Искључити блокатор банера колачића за %1$s? %1$s не може самостално одбити захтеве за употребу колачића на овој страници. Можете послати захтев за подржавање одбијања на овом сајту у будућности. %1$s ће обрисати колачиће и освежити страницу. Брисање колачића може да вас одјави са сајта или да испразни вашу корпу за куповину. + + Искључите и %1$s ће обрисати колачиће и поново учитати овај сајт. Ово вас може одјавити са налога или испразнити вашу корпу за куповину. %1$s покушава аутоматски да одбије све захтеве за колачиће на подржаним сајтовима. + + Укључите и %1$s ће покушати да аутоматски одбије све банере колачића на овом сајту. Дозволити да %1$s одбаци банере колачића? @@ -475,6 +524,11 @@ Дозволи + + %1$s је управо одбио колачиће уместо вас + + Мање ометања, мање колачића који вас прате на овом сајту. + Самостално се повезујемо на странице користећи HTTPS протокол за шифровање података у преносу зарад боље безбедности. @@ -499,10 +553,14 @@ Приступачност Прилагођени сервер Firefox налога + + Прилагођени сервер Mozilla налога Прилагођени Sync сервер Firefox налог/Sync сервер је промењен. Затворите апликацију да би промене ступиле на снагу… + + Mozilla налог/сервер за синхронизацију је измењен. Затварање апликације ради примене промена… Налог @@ -551,6 +609,16 @@ Подешавања налога Аутоматско завршавање адреса + + Спонзорисани предлози + + Подржите %1$s повременим спонзорисаним предлозима + + Предлози од %1$s + + Добијајте предлоге са интернета у вези са вашим претрагама Отвори везе у апликацијама @@ -562,9 +630,16 @@ Спољни управник преузимања + + Омогућите Gecko евидентирање + + Затварање апликације ради примене промена… + Додаци + + Инсталирајте додатак из датотеке Обавештења @@ -2065,7 +2140,7 @@ - Firefox предлози + Firefox предлог Google претрага @@ -2115,10 +2190,48 @@ Подешавања Сазнајте више + + Проверите сада Производ није доступан + + Пријавите да је производ на залихама + + Пријавите да је производ на залихама + + Провера квалитета рецензија + + Провера квалитета рецензија Ово може да потраје око 60 секунди. + + Хвала што сте пријавили! + + Требало би да имамо информације о рецензијама овог производа у року од 24 сата. Навратите касније. + + Не можемо да проверимо ове рецензије + + Нажалост, не можемо да проверимо квалитет рецензија за одређене врсте производа, као што су: поклон картице и стриминг видеа, музике и игара. + + Информације стижу ускоро + + Требало би да имамо информације о рецензијама овог производа у року од 24 сата. Навратите касније. + + Анализа је ажурна + + Важи + + Тренутно нема доступних информација + + Радимо на решавању проблема. Покушајте поново касније. + + Нема интернет везе + + Проверите вашу интернет везу, па покушајте да поново учитате страницу. + + Још увек нема информација о овим рецензијама + + Сазнајте више политика приватности @@ -2163,4 +2276,4 @@ прочитајте чланак отворите везу да сазнате више - + diff --git a/app/src/main/res/values-su/strings.xml b/app/src/main/res/values-su/strings.xml index 17ae56f1a8..e2207f7515 100644 --- a/app/src/main/res/values-su/strings.xml +++ b/app/src/main/res/values-su/strings.xml @@ -72,6 +72,11 @@ The first parameter is the name of the app defined in app_name (for example: Firefox Nightly) The second parameter is the clickable link text in felt_privacy_info_card_subtitle_link_text --> %1$s mupus réréméh, jujutan, jeung data loka anjeun nalika anjeun nutup sakabéh jandéla nyamuni. %2$s + + %1$s mupus réréméh, jujutan, jeung data loka anjeun nalika anjeun nutup sakabéh jandéla nyamuni. %2$s @@ -200,6 +205,8 @@ Pabukon Loka déstop + + Buka dina tab biasa Tambahkeun ka layar Tepas @@ -257,7 +264,7 @@ Pinday - Mesin pamaluruh + Mesin pamaluruh Setélan mesin pamaluruh @@ -389,6 +396,8 @@ Pilih salasahiji Kokolakeun takulan pamaluruhan + + Kokolakeun mesin pamaluruh lianna Mesin édit némbongan dina menu pamaluruhan @@ -399,8 +408,16 @@ Paluruh Mesin pamaluruh + + Usulan ti mesin pamaluruh Palang alamat + + Préperénsi palang alamat + + Palang alamat - Firefox Suggest + + Leuwih teleb ngeunaan Firefox Suggest Peunteun dina Google Play - Kurangan Spanduk Réréméh + Kurangan Spanduk Réréméh + + Pameungpeuk Spanduk Réréméh - Kurangan spanduk réréméh + Kurangan spanduk réréméh - Pareum + Pareum - Hurung + Hurung - %1$s otomatis nyoba nolak rekés réréméh dina spanduk réréméh. + %1$s otomatis nyoba nolak rekés réréméh dina spanduk réréméh. Pareum jang ieu loka @@ -456,26 +475,26 @@ Kiwari loka teu didukung - Hurungkeun Reduksi spanduk réréméh pikeun %1$s? + Hurungkeun Reduksi spanduk réréméh pikeun %1$s? - Pareuman Reduksi spanduk réréméh pikeun %1$s? + Pareuman Reduksi spanduk réréméh pikeun %1$s? %1$s teu bisa otomatis nolak rekés réréméh dina ieu loka. Anjeun bisa ngirim rekés pikeun ngadukung ieu loka jaga. - %1$s bakal mupus réréméh ieu loka tur muka ulang kacana. Mupus sadaya réréméh bisa ngaluarkeun login anjeun atawa ngosongkeun karanjang balanja. + %1$s bakal mupus réréméh ieu loka tur muka ulang kacana. Mupus sadaya réréméh bisa ngaluarkeun login anjeun atawa ngosongkeun karanjang balanja. - %1$s nyoba sacara otomatis nolak sakur rekés réréméh di loka anu didukung. + %1$s nyoba sacara otomatis nolak sakur rekés réréméh di loka anu didukung. - Idinan %1$s nampik spanduk réréméh? + Idinan %1$s nampik spanduk réréméh? - %1$s bisa otomatis nampik loba rekés spanduk réréméh. + %1$s bisa otomatis nampik loba rekés spanduk réréméh. - Moal Waka + Moal Waka - Anjeun bakal leuwih saeutik nempo rekés réréméh + Anjeun bakal leuwih saeutik nempo rekés réréméh - Idinan + Idinan Otomatis nyoba nyambung ka loka maké protokol énkripsi HTTPS pikeun ngaronjatkeun kaamanan. @@ -521,6 +540,8 @@ Asup pikeun nyingkronkeun tab, markah, kecap sandi, jeung sajabana. Akun Firefox + + Akun Mozilla Sambungkeun deui pikeun neruskeun nyingkronkeun @@ -554,6 +575,9 @@ Otokumplit URLs + + Saran ti %1$s Buka tutumbu dina aplikasi @@ -636,16 +660,10 @@ %s klasik - - Édisi Diwates Séri artis - - Kumpulan Independent Voices anyar. %s Koléksi Independent Voices. %s - - Kumpulan Independent Voices anyar. Koléksi Independent Voices. @@ -1398,14 +1416,11 @@ %d tab - Jujutan nyungsi jeung data loka Jujutan langlangan %d alamat - - Réréméh Réréméh jeung data loka @@ -1467,63 +1482,10 @@ Grup dihapus - - Wilujeng sumping di internét anu leuwih hadé - - Panyungsi anu diwangun pikeun jalma, lain bati. - - Angkut di tempat nu ku anjeun tinggalkeun - - Singkronkeun tab jeung kecap sandi dina sadaya paranti pikeun pindah layar anu mulus. - - Asup Singkronna hurung - - Salindung pripasi sacara baku - - %1$s otomatis megat maskapé rerencepan nunutur anjeun ngalanglang raramat. - - Miturkeun Total Cookie Protection pikeun ngeureunkeun palacak tina maké réréméh pikeun ngintip anjeun meuntas loka. - - Baku (bawaan) - - Siger tengah pikeun pripasi jeung kinerja. Kaca dimuat sacara normal. - - Pereketkeun - - Meungpeuk palacak leuwih loba sangkan kaca dimuat leuwih gancang, tapi sababaraha fungsionalitas dina kaca bisa gagal. - - Pilih perenah tulbar anjeun - - Angger di handap, atawa pindahkeun ka luhur. - - Anjeun ngadalikeun data anjeun - - Firefox ngatur sangkan anjeun bisa nangtukeun naon anu dibagikeun onlén jeung naon anu dibagikeun ka kami. - - Maca wawaran salindungan kami - - - Siap muka internét anu hébring? - - Mitembeyan nyungsi - - - Pilih téma anjeun - - Ngirit batré jeung jaga kaséhatan mata maké mode poék. - - Otomatis - - Luyukeun jeung setélan paranti - - Téma poék - - Téma caang - Tab dikirim! @@ -1971,8 +1933,6 @@ URL API kamandang maluruh - Ganti kueri ku “%s”. Conto:\nhttp://suggestqueries.google.com/complete/search?client=firefox&q=%s - Ganti kueri ku “%s”. Conto:\nhttps://suggestqueries.google.com/complete/search?client=firefox&q=%s Teundeun @@ -2193,6 +2153,28 @@ %s ku Mozilla + + Ngarti + + Euweuh sambungan jaringan + + Leuwih teleb + + kawijakan pripasi + + katangtuan maké + + Enya, cobaan + + Moal waka + + Cobaan pamariksa ténjoan + + Buka pamariksa ténjoan + + + Béta + tilep @@ -2204,4 +2186,4 @@ baca artikel buka tutumbu pikeun ngalenyepan - + diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 7cd381e555..3a7df1b570 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -528,6 +528,11 @@ Tillåt + + %1$s har precis avvisat kakor åt dig + + Mindre distraktioner, färre kakor som spårar dig på den här webbplatsen. + Försöker automatiskt ansluta till webbplatser med HTTPS-krypteringsprotokollet för ökad säkerhet. @@ -638,6 +643,8 @@ Tillägg + + Installera tillägg från fil Aviseringar @@ -2256,8 +2263,6 @@ Annons av %s - Recensionsgranskaren drivs av %s. - Recensionsgranskaren drivs av %s %s av Mozilla @@ -2317,6 +2322,8 @@ Prova vår pålitliga guide till produktrecensioner Se hur tillförlitliga produktrecensioner är på %1$s innan du köper. Recensionsgranskaren, en experimentell funktion från %2$s, är inbyggd direkt i webbläsaren. Den fungerar på %3$s och %4$s också. + + Se hur tillförlitliga produktrecensioner är på %1$s innan du köper. Recensionsgranskaren, en experimentell funktion från %2$s, är inbyggd direkt i webbläsaren. Med kraften i %1$s från Mozilla hjälper vi dig att undvika partiska och oäkta recensioner. Vår AI-modell förbättras alltid för att skydda dig när du handlar. %2$s @@ -2383,4 +2390,6 @@ läs artikeln öppna länken för att lära dig mer + + %s, Rubrik diff --git a/app/src/main/res/values-tg/strings.xml b/app/src/main/res/values-tg/strings.xml index 016cc0687b..0e747d3f24 100644 --- a/app/src/main/res/values-tg/strings.xml +++ b/app/src/main/res/values-tg/strings.xml @@ -463,16 +463,20 @@ Реҷаи «Танҳо HTTPS» - Маҳдудкунии баннери куки + Маҳдудкунии баннери куки + + Манъкунандаи баннери куки + + Манъкунандаи баннери куки дар тамошокунии хусусӣ - Маҳдуд кардани баннерҳои куки + Маҳдуд кардани баннерҳои куки - Ғайрифаъол + Ғайрифаъол - Фаъол + Фаъол - «%1$s» ба таври худкор кушиш мекунад, ки дархостҳои кукиҳоро дар баннерҳои кукиҳо рад кунад. + «%1$s» ба таври худкор кушиш мекунад, ки дархостҳои кукиҳоро дар баннерҳои кукиҳо рад кунад. Барои ин сомона хомӯш аст @@ -490,17 +494,25 @@ Ин сомона дар айни замон дастгирӣ намешавад - «Маҳдудкунии баннери куки»-ро барои %1$s фаъол месозед? + «Маҳдудкунии баннери куки»-ро барои %1$s фаъол месозед? + + Манъкунандаи баннери кукиро барои %1$s фаъол мекунед? - «Маҳдудкунии баннери куки»-ро барои %1$s хомӯш месозед? + «Маҳдудкунии баннери куки»-ро барои %1$s хомӯш месозед? + + Манъкунандаи баннери кукиро барои %1$s хомӯш мекунед? «%1$s» наметавонад, ки дархостҳои кукиро барои ин сомона ба таври худкор рад кунад. Шумо метавонед барои дастгирӣ кардани ин сомона дар оянда дархостеро фиристонед. - %1$s кукиҳои ин сомонаро тоза мекунад ва саҳифаро аз нав бор мекунад. Амали тозакунии ҳамаи кукиҳо метавонад шуморо аз сомона хориҷ кунад ва сабадҳои харидории шуморо холӣ намояд. + %1$s кукиҳои ин сомонаро тоза мекунад ва саҳифаро аз нав бор мекунад. Амали тозакунии ҳамаи кукиҳо метавонад шуморо аз сомона хориҷ кунад ва сабадҳои харидории шуморо холӣ намояд. + + Хомӯш кунед ва «%1$s» кукиҳоро тоза намуда, ин сомонаро аз нав бор мекунад. Ин амал метавонад шуморо аз сомона хориҷ кунад ва сабадҳои харидории шуморо холӣ намояд. - «%1$s» кӯшиш мекунад, ки ҳамаи дархостҳои кукиҳоро дар сомонаҳои дастгиришаванда ба таври худкор рад кунад. + «%1$s» кӯшиш мекунад, ки ҳамаи дархостҳои кукиҳоро дар сомонаҳои дастгиришаванда ба таври худкор рад кунад. + + Фаъол созед, ва «%1$s» кӯшиш мекунад, ки ҳамаи баннерҳои кукиро дар ин сомона ба таври худкор рад кунад. Ба «%1$s» иҷозат медиҳед, ки баннерҳои кукиро рад кунад? @@ -513,6 +525,11 @@ Иҷозат додан + + «%1$s» дар ҳоли ҳозир барои шумо кукиҳоро рад кард + + Камтар ҳалалҳо ва камтар кукиҳое, ки шуморо дар ин сомона пайгирӣ мекунанд. + Ба таври худкор кӯшиш мекунад, ки ба сомонаҳо бо истифода аз протоколи рамзгузории HTTPS барои баланд бардоштани амният пайваст шавад. @@ -620,6 +637,8 @@ Ҷузъҳои иловагӣ + + Насб кардани ҷузъи иловагӣ аз файл Огоҳномаҳо @@ -2240,8 +2259,6 @@ Реклама аз ҷониби «%s» - Абзори тафтиши тақризҳо аз ҷониби «%s» таҳия карда шудааст. - Абзори тафтиши тақризҳо аз ҷониби «%s» таҳия карда шудааст «%s» аз ҷониби «Mozilla» @@ -2301,6 +2318,8 @@ Дастури моро дар бораи эътимоднокӣ нисбат ба тақризҳои маҳсулот озмоед Пеш аз хариди маҳсул, дар «%1$s» аз назар гузаронед, ки то чӣ андоза тақризҳо дар бораи ин маҳсул боэътимод мебошанд. Абзори тафтиши тақризҳо ҳамчун хусусияти озмоишӣ аз тарафи «%2$s» ба браузер дарунсохт карда шудааст. Ин хусусият ҳам дар «%3$s» ва ҳам дар «%4$s» кор мекунад. + + Пеш аз хариди маҳсул, дар «%1$s» аз назар гузаронед, ки то чӣ андоза тақризҳо дар бораи ин маҳсул боэътимод мебошанд. Абзори тафтиши тақризҳо ҳамчун хусусияти озмоишӣ аз тарафи «%2$s» ба браузер дарунсохт карда шудааст. Бо истифода аз «%1$s» аз ҷониби «Mozilla», мо ба шумо барои истисно кардани тақризҳои ғаразнок ва ғайримуқаррарӣ кумак мекунем. Намунаи зеҳни сунъии (AI)-ии мо барои муҳофизат кардани раванди харидории шумо доим такмил дода мешавад. %2$s @@ -2367,4 +2386,6 @@ мақоларо хонед барои маълумоти бештар пайвандро кушоед + + %s, Сарлавҳа diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index d03a8a1f89..644d8141af 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -205,6 +205,8 @@ ห้องสมุด ไซต์เดสก์ท็อป + + เปิดในแท็บปกติ เพิ่มไปยังหน้าจอหลัก @@ -261,7 +263,7 @@ สแกน - เครื่องมือค้นหา + เครื่องมือค้นหา การตั้งค่าเครื่องมือค้นหา @@ -335,6 +337,8 @@ Firefox ให้ความสำคัญกับผู้คนมากกว่าผลกำไรและปกป้องความเป็นส่วนตัวของคุณด้วยการปิดกั้นเครื่องมือติดตามข้ามไซต์\n\nเรียนรู้เพิ่มเติมในประกาศความเป็นส่วนตัวของเรา + + เบราว์เซอร์ที่ได้รับการสนับสนุนจากองค์กรไม่แสวงหาผลกำไรของเราช่วยหยุดบริษัทต่าง ๆ ไม่ให้แอบติดตามคุณทางเว็บได้\n\nเรียนรู้เพิ่มเติมในประกาศความเป็นส่วนตัวของเรา ประกาศความเป็นส่วนตัว @@ -348,6 +352,9 @@ คงการเข้ารหัสเมื่อคุณเปลี่ยนจากอุปกรณ์เครื่องหนึ่งไปยังอีกเครื่องหนึ่ง นำแท็บและรหัสผ่านจากอุปกรณ์อื่น ๆ ของคุณเข้ามาเพื่อเรียกดูต่อจากที่คุณค้างไว้ + + เมื่อคุณลงชื่อเข้าและซิงค์แล้ว คุณจะปลอดภัยยิ่งขึ้น Firefox จะเข้ารหัสลับรหัสผ่าน ที่คั่นหน้า และอื่น ๆ ของคุณ ลงชื่อเข้า @@ -419,6 +426,10 @@ คำแนะนำจากเครื่องมือค้นหา แถบที่อยู่ + + การกำหนดลักษณะแถบที่อยู่ + + แถบที่อยู่ - Firefox Suggest เรียนรู้เพิ่มเติมเกี่ยวกับ Firefox Suggest @@ -449,16 +460,20 @@ โหมด HTTPS-Only - การลดคุกกี้แบนเนอร์ + การลดคุกกี้แบนเนอร์ + + ตัวปิดกั้นแบนเนอร์คุกกี้ + + ตัวปิดกั้นแบนเนอร์คุกกี้ในการเรียกดูแบบส่วนตัว - ลดคุกกี้แบนเนอร์ + ลดคุกกี้แบนเนอร์ - ปิด + ปิด - เปิด + เปิด - %1$s จะพยายามปฏิเสธคำขอคุกกี้บนแบนเนอร์คุกกี้โดยอัตโนมัติ + %1$s จะพยายามปฏิเสธคำขอคุกกี้บนแบนเนอร์คุกกี้โดยอัตโนมัติ ปิดสำหรับไซต์นี้ @@ -477,28 +492,39 @@ ไม่รองรับไซต์ในขณะนี้ - ต้องการเปิดการลดแบนเนอร์คุกกี้สำหรับ %1$s หรือไม่? + ต้องการเปิดการลดแบนเนอร์คุกกี้สำหรับ %1$s หรือไม่? + + ต้องการเปิดตัวปิดกั้นแบนเนอร์คุกกี้สำหรับ %1$s หรือไม่? + + ต้องการปิดการลดแบนเนอร์คุกกี้สำหรับ %1$s หรือไม่? - ต้องการปิดการลดแบนเนอร์คุกกี้สำหรับ %1$s หรือไม่? + ต้องการปิดตัวปิดกั้นแบนเนอร์คุกกี้สำหรับ %1$s หรือไม่? %1$s ไม่สามารถปฏิเสธคำขอคุกกี้โดยอัตโนมัติบนไซต์นี้ คุณสามารถส่งคำขอให้รองรับไซต์นี้ในอนาคตได้ - %1$s จะล้างคุกกี้ของไซต์นี้และรีเฟรชหน้า การล้างคุกกี้ทั้งหมดอาจนำคุณออกจากระบบหรือล้างรถเข็นช็อปปิ้ง + %1$s จะล้างคุกกี้ของไซต์นี้และรีเฟรชหน้า การล้างคุกกี้ทั้งหมดอาจนำคุณออกจากระบบหรือล้างรถเข็นช็อปปิ้ง + + หลังจากปิดแล้ว %1$s จะล้างคุกกี้และโหลดไซต์นี้ใหม่ การกระทำนี้อาจนำคุณลงชื่อออกหรือล้างรถเข็นช็อปปิ้ง + + %1$s จะพยายามปฏิเสธคำขอคุกกี้ทั้งหมดบนไซต์ที่รองรับโดยอัตโนมัติ - %1$s จะพยายามปฏิเสธคำขอคุกกี้ทั้งหมดบนไซต์ที่รองรับโดยอัตโนมัติ + หลังจากเปิดแล้ว %1$s จะพยายามปฏิเสธแบนเนอร์คุกกี้ทั้งหมดบนไซต์นี้โดยอัตโนมัติ - อนุญาตให้ %1$s ปฏิเสธแบนเนอร์คุกกี้หรือไม่? + อนุญาตให้ %1$s ปฏิเสธแบนเนอร์คุกกี้หรือไม่? - %1$s สามารถปฏิเสธคำขอคุกกี้จำนวนมากได้โดยอัตโนมัติ + %1$s สามารถปฏิเสธคำขอคุกกี้จำนวนมากได้โดยอัตโนมัติ - ไม่ใช่ตอนนี้ + ไม่ใช่ตอนนี้ - คุณจะเห็นคำขอคุกกี้น้อยลง + คุณจะเห็นคำขอคุกกี้น้อยลง - อนุญาต + อนุญาต + + + รบกวนสมาธิน้อยลง คุกกี้ติดตามคุณน้อยลงบนเว็บไซต์นี้ พยายามเชื่อมต่อกับเว็บไซต์โดยใช้โปรโตคอลการเข้ารหัส HTTPS โดยอัตโนมัติเพื่อเพิ่มความปลอดภัย @@ -609,6 +635,8 @@ ส่วนเสริม + + ติดตั้งส่วนเสริมจากไฟล์ การแจ้งเตือน @@ -689,6 +717,8 @@ ส่วนเสริมใหม่ที่ใช้งานได้ในขณะนี้ + + ลองดูส่วนขยายใหม่กว่า 100 รายการที่ให้คุณสร้าง Firefox ในแบบของคุณเอง สำรวจส่วนเสริม @@ -2198,7 +2228,9 @@ เรียนรู้เพิ่มเติมเกี่ยวกับ %s - วิธีที่ %s โดย Mozilla พิจารณาคุณภาพบทวิจารณ์ + วิธีที่ %s โดย Mozilla พิจารณาคุณภาพบทวิจารณ์ + + วิธีที่ %s พิจารณาคุณภาพบทวิจารณ์ การตั้งค่า @@ -2222,6 +2254,8 @@ สินค้าไม่พร้อมจำหน่าย + รายงานสินค้านี้ได้กลับมาในสต็อก + รายงานสินค้ามีในสต็อก กำลังตรวจสอบคุณภาพบทวิจารณ์ @@ -2243,6 +2277,8 @@ ไม่มีข้อมูลในขณะนี้ ไม่มีการเชื่อมต่อเครือข่าย + + ตรวจสอบการเชื่อมต่อเครือข่ายของคุณแล้วลองโหลดหน้าใหม่ ยังไม่มีข้อมูลเกี่ยวกับบทวิจารณ์เหล่านี้ @@ -2250,9 +2286,13 @@ เรียนรู้เพิ่มเติม - นโยบายความเป็นส่วนตัว + นโยบายความเป็นส่วนตัว + + นโยบายความเป็นส่วนตัว + + เงื่อนไขการใช้งาน - เงื่อนไขการใช้งาน + เงื่อนไขการใช้งาน ใช่ลองดู @@ -2301,4 +2341,4 @@ อ่านบทความ เปิดลิงก์เพื่อเรียนรู้เพิ่มเติม - + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 4cc79157c5..e0fc1db417 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -340,6 +340,8 @@ Firefox için paradan önce insanlık gelir. Siteler arası takip kodlarını engelleyerek gizliliğinizi koruyoruz.\n\nAyrıntıları gizlilik bildirimimizde bulabilirsiniz. + + Kâr amacı gütmeyen tarayıcımız, şirketlerin sizi web’de gizlice takip etmesini engelliyor.\n\nAyrıntıları gizlilik bildirimimizde bulabilirsiniz. gizlilik bildirimimizde @@ -461,16 +463,20 @@ Yalnızca HTTPS modu - Çerez bildirimlerini azalt + Çerez bildirimlerini azalt + + Çerez bildirimi engelleyici + + Gizli gezintide çerez bildirimi engelleyici - Çerez bildirimlerini azalt + Çerez bildirimlerini azalt - Kapalı + Kapalı - Açık + Açık - %1$s, çerez bildirimlerindeki çerez isteklerini otomatik olarak reddetmeye çalışır. + %1$s, çerez bildirimlerindeki çerez isteklerini otomatik olarak reddetmeye çalışır. Bu sitede kapalı @@ -488,16 +494,24 @@ Bu site şu anda desteklenmiyor - %1$s için çerez bildirimlerini azaltma açılsın mı? + %1$s için çerez bildirimlerini azaltma açılsın mı? + + %1$s için çerez bildirimlerini engelleme açılsın mı? + + %1$s için çerez bildirimlerini azaltma kapatılsın mı? - %1$s için çerez bildirimlerini azaltma kapatılsın mı? + %1$s için çerez bildirimlerini engelleme kapatılsın mı? %1$s bu sitedeki çerez isteklerini otomatik olarak reddedemiyor. İleride bu siteyi desteklememiz için istekte bulunabilirsiniz. - %1$s bu sitenin çerezlerini temizleyip sayfayı tazeleyecek. Tüm çerezlerin temizlenmesi oturumunuzu kapatabilir veya alışveriş sepetlerinizi boşaltabilir. + %1$s bu sitenin çerezlerini temizleyip sayfayı tazeleyecek. Tüm çerezlerin temizlenmesi oturumunuzu kapatabilir veya alışveriş sepetlerinizi boşaltabilir. + + Bunu kapatırsanız %1$s çerezleri temizleyip bu siteyi yeniden yükleyecektir. Bu durumda oturumunuz kapanabilir veya alışveriş sepetleriniz boşalabilir. + + %1$s, desteklenen sitelerdeki tüm çerez isteklerini otomatik olarak reddetmeye çalışır. - %1$s, desteklenen sitelerdeki tüm çerez isteklerini otomatik olarak reddetmeye çalışır. + Açtığınızda %1$s bu sitedeki tüm çerez bildirimlerini otomatik olarak reddetmeye çalışacaktır. %1$s çerez bildirimlerini reddedebilsin mi? @@ -510,6 +524,12 @@ İzin ver + + %1$s sizin için çerezleri reddetti + + + Bu site dikkatinizi daha az dağıtacak, çerezler sizi daha az izleyecek. + Daha fazla güvenlik için sitelere otomatik olarak HTTPS şifreleme protokolüyle bağlanmaya çalışır. @@ -620,6 +640,8 @@ Eklentiler + + Dosyadan eklenti yükle Bildirimler @@ -2189,6 +2211,8 @@ Öne çıkan son değerlendirmeler Değerlendirme kalitesini nasıl belirliyoruz? + + Ürün değerlendirmelerinin güvenilirliğini denetlemek için Mozilla’nın %s yapay zekâ teknolojisini kullanıyoruz. Bu araç yalnızca değerlendirmelerin kalitesini anlamanıza yardımcı olur, ürün kalitesi hakkında yorum yapmaz. harf notu veriyoruz.]]> @@ -2223,11 +2247,11 @@ Daha fazla bilgi alın Değerlendirme kontrolcüsünü kapat + + İlginizi çekebilir %s reklamı - Değerlendirme kontrolcüsünün altyapısı %s tarafından sağlanmaktadır. - Değerlendirme kontrolcüsünün altyapısı %s tarafından sağlanmaktadır Mozilla’dan %s @@ -2285,8 +2309,18 @@ Değerlendirme kalitesini kontrol et Ürün değerlendirmelerine ilişkin güvenilir kılavuzumuzu deneyin + + Yeni bir şey satın almadan önce %1$s sitesindeki ürün değerlendirmelerinin güvenilirliğini görün. Deneysel bir %2$s özelliği olan değerlendirme kontrolcüsü, tarayıcınızla birlikte geliyor. %3$s ve %4$s ile de uyumlu. + + Yeni bir şey satın almadan önce %1$s sitesindeki ürün değerlendirmelerinin güvenilirliğini görün. Deneysel bir %2$s özelliği olan değerlendirme kontrolcüsü, tarayıcınızla birlikte geliyor. + + Mozilla %1$s sayesinde önyargılı ve sahte değerlendirmelerden kaçınabilirsiniz. Alışverişlerinizde sizi korumak için yapay zekâ modelimizi sürekli geliştiriyoruz. %2$s Daha fazla bilgi alın + + “Evet, deneyeceğim”i seçtiğinizde Mozilla %1$s %2$s ve %3$snı kabul etmiş olursunuz. + + “Evet, deneyeceğim”i seçtiğinizde %1$s için şunları kabul etmiş olursunuz: gizlilik politikası @@ -2347,4 +2381,6 @@ makaleyi oku bilgi almak için bağlantıyı aç + + %s, Başlık diff --git a/app/src/main/res/values-ug/strings.xml b/app/src/main/res/values-ug/strings.xml index e03c4e50fc..cc1f2819df 100644 --- a/app/src/main/res/values-ug/strings.xml +++ b/app/src/main/res/values-ug/strings.xml @@ -315,6 +315,9 @@ ئۇقتۇرۇشلار %s بىلەن تېخىمۇ كۆپ ئىشلارنى قىلىشىڭىزغا ياردەم بېرىدۇ + + بەتكۈچلىرىڭىزنى ئۈسكۈنىلەر ئارا قەدەمداشلايدۇ، چۈشۈرۈشنى باشقۇرىدۇ، %s نىڭ شەخسىيەت قوغدىشى ۋە باشقا تېخىمۇ كۆپ ئىشلىتىش ھەققىدىكى تەكلىپلەرگە ئېرىشتۈرىدۇ. داۋاملاشتۇر @@ -326,6 +329,11 @@ Firefox نى ئاساسىي تور كۆرگۈڭىزگە ئايلاندۇرۇڭ بىخەتەرلىكىڭىزنى ساقلاشنى ياخشى كۆرىمىز + + Firefox تور بېكەتتىكى ئىز قوغلىغۇچلارنى توسۇش ئارقىلىق كىشىلەرنى مەنپەئەتتىن ئۈستۈن قىلىدۇ ۋە شەخسىيىتىڭىزنى قوغدايدۇ.\n\nشەخسىيەت ئۇقتۇرۇشىمىزدىن كۆپرەك ئۆگىنەلەيسىز. + + پايدا تاپمايدىغان ئورگان قوللايدىغان توركۆرگۈمىز شىركەتلەرنىڭ سىزنى مەخپىي ھالدا توردىن ئىزلىشىنى توختىتىشىغا ياردەم بېرىدۇ.\n\nشەخسىيەت ئۇقتۇرۇشى ھەققىدىكى تەپسىلات بىلدۈرگۈسى. شەخسىيەت ئۇقتۇرۇشى @@ -339,6 +347,9 @@ ئۈسكۈنىلەر ئارا ئالماشتۇرغاندا شىفىرلىنىدۇ باشقا ئۈسكۈنىلىرىڭىزدىكى بەتكۈچ ۋە ئىملارنى قەدەمداشلاپ، كەلگەن جايدىن داۋاملاشتۇرالايسىز. + + تىزىمغا كىرىپ قەدەمداشلىسىڭىز، تېخىمۇ بىخەتەر بولىسىز. Firefox ئىم، خەتكۈچ ۋە باشقىلارنى مەخپىيلەشتۈرىدۇ. تىزىمغا كىرىڭ @@ -349,6 +360,12 @@ ئۇقتۇرۇشلار Firefox ئارقىلىق تېخىمۇ بىخەتەر بولۇشىڭىزغا ياردەم بېرىدۇ + + ئۈسكۈنىلەر ئارا بەتكۈچ ئەۋەتىپ، چۈشۈرۈشنى باشقۇرۇپ ۋە Firefox تىن ئەڭ ياخشى پايدىلىنىشقا ئائىت تەكلىپلەرگە ئېرىشەلەيسىز. + + ئۈسكۈنىڭىز ئارىسىدا بەتكۈچلەرنى بىخەتەر ئەۋەتىپ، Firefox دىكى باشقا شەخسىيەت ئىقتىدارلىرىنى بايقايدۇ. ئۇقتۇرۇشنى ئاچ @@ -356,6 +373,9 @@ Firefox ئىزدەش ئەپچەنى سىناڭ + + باش ئېكرانىڭىزغا Firefox نى جايلاشتۇرسىڭىز، شەخسىيەتنى ئەڭ ئالدىنقى ئورۇنغا قويىدىغان تور كۆرگۈنى خالىغان ۋاقىتتا زىيارەت قىلالايسىز، ئۇ بېكەت ھالقىغان ئىز قوغلاشنى توسىدىغان تور كۆرگۈ. Firefox ئەپچە قوش @@ -445,6 +465,8 @@ تاقاق ئوچۇق + + %1$s ئۆزلۈكىدىن cookie لوزۇنكىسىنىڭ cookie ئىلتىماسىنى رەت قىلىشنى سىنايدۇ. بۇ تور بېكەتكە تاقاق @@ -481,6 +503,9 @@ يول قوي + + %1$s سىز ئۈچۈن ساقلانمىنى رەت قىلدى + تاقاق @@ -582,6 +607,8 @@ قوشۇلما + + ھۆججەتتىن قوشۇلما ئورنىتىدۇ ئۇقتۇرۇش @@ -2056,10 +2083,210 @@ تېخىمۇ كۆپ بايقاش تەمىنلىگۈچى %1$s. + + Firefox جەمەتىنىڭ بىر قىسمى. %s تەپسىلاتى + + قوللىغان + + + سانلىق مەلۇمات ئەۋەتىشتە تېلېگرافنى قوزغىتىدۇ. تەڭشەكنى ئاچ + + + باھالاش تەكشۈرگۈچ + + باھالاش تەكشۈرگۈچ + + ئىشەنچلىك باھالاش + + + ئىشەنچلىك ۋە ئىشەنچسىز باھالارنىڭ ئارىلاشمىسى + + ئىشەنچسىز باھالاش + + بۇ باھالار قانچىلىك ئىشەنچلىك؟ + + تەڭشەلگەن باھا + + ئىشەنچسىز باھا چىقىرىۋېتىلدى + + يېقىنقى باھالاردىن يارقىن نۇقتىلار + + تەكشۈرۈش سۈپىتىنى قانداق بېكىتىمىز + + بىز Mozilla نىڭ %s دىكى سۈنئىي ئەقىل تېخنىكىسىنى ئىشلىتىپ مەھسۇلات باھالىنىشىنىڭ ئىشەنچلىكلىكىنى تەكشۈردۇق. بۇ مەھسۇلات سۈپىتىنى ئەمەس، بەلكى باھالاش سۈپىتىنى باھالىشىڭىزغا ياردەم بېرىدۇ. + + ھەرپ دەرىجىسى تەقسىملەيمىز.]]> + + + ئىشەنچلىك باھالاش. ئىشىنىمىزكى، بۇ باھالاشنى سەمىمىي، تەرەپسىز ھەقىقىي خېرىدارلار بەرگەن بولۇشى مۇمكىن. + + باھالارنىڭ ئىشەنچلىك ئىكەنلىكىگە ئىشىنىمىز. + + ئىشەنچلىك ۋە ئىشەنچسىز باھالارنىڭ بارلىقىغا ئىشىنىمىز. + + ئىشەنچسىز باھالاش. بىز بۇنى ساختا ياكى بىر تەرەپلىمە باھالىغۇچىلار باھالىغان بولۇشى مۇمكىن دەپ قارايمىز. + + بىز باھالارنىڭ ئىشەنچلىك ئەمەسلىكىگە ئىشىنىمىز. + + تەڭشەلگەن باھا پەقەت بىز ئىشەنچلىك دەپ قارىغان باھالارنىلا ئاساس قىلىدۇ.]]> + + يارقىن نۇقتىلار بىز ئىشەنچلىك دەپ قارىغان يېقىنقى 80 كۈن ئىچىدىكى %s باھادىن كەلگەن.]]> + + %s ھەققىدىكى تەپسىلاتلار. + + Mozilla قانداق قىلىپ %s نىڭ تەكشۈرۈش سۈپىتىنى بەلگىلەيدۇ + + %s تەكشۈرۈش سۈپىتىنى قانداق بەلگىلەيدۇ + + تەڭشەكلەر + + باھالاش تەكشۈرگۈچتە ئېلان كۆرسىتىدۇ + + + مۇناسىۋەتلىك مەھسۇلاتلارنىڭ ئېلانلىرىنى ئاندا-ساندا كۆرىسىز. بارلىق ئېلانلار چوقۇم باھالاش سۈپىتى ئۆلچىمىمىزگە ماس كېلىشى كېرەك. %s + + + مۇناسىۋەتلىك مەھسۇلاتلارنىڭ ئېلانلىرىنى ئاندا-ساندا كۆرىسىز. بىز پەقەت ئىشەنچلىك باھالانغان مەھسۇلاتلارغا ئېلان بېرىمىز. %s + + تەپسىلاتى + + باھالاش تەكشۈرگۈچنى تاقا + + تېخىمۇ كۆپ مەھسۇلاتلار + + ئېلان بەرگۈچى %s + + باھالاش تەكشۈرگۈچنى تەمىنلىگۈچى %s + + Mozilla قوللىغان %s + + تەكشۈرىدىغان يېڭى ئۇچۇرلار + + ھازىر تەكشۈر + + تېخى يېتەرلىك باھالىغۇچى يوق + + + بۇ مەھسۇلاتنىڭ باھالىنىشى تېخىمۇ كۆپ بولغاندا، بىز ئۇلارنىڭ سۈپىتىنى تەكشۈرەلەيمىز. + + + مەھسۇلات يوق + + ئەگەر بۇ مەھسۇلاتتىن مال بارلىقىنى بايقىسىڭىز، دوكلات قىلسىڭىز، بىز باھالاشنى تەكشۈرەلەيمىز. + + بۇ مەھسۇلاتتىن مال بارلىقىنى دوكلات قىلىڭ + + مەھسۇلاتنىڭ تىزىلغانلىقىنى دوكلات قىلىڭ + + باھالاش سۈپىتىنى تەكشۈرۈۋاتىدۇ + + باھالاش سۈپىتىنى تەكشۈرۈۋاتىدۇ + + بۇنىڭغا 60 سېكۇنت ئەتراپىدا ۋاقىت كېتىشى مۇمكىن. + + دوكلات قىلغانلىقىڭىزغا رەھمەت! + + + بىز 24 سائەت ئىچىدە بۇ مەھسۇلاتنىڭ باھالىنىشىغا مۇناسىۋەتلىك ئۇچۇرغا ئېرىشىمىز. قايتا كېلىپ تەكشۈرۈپ بېقىڭ. + + بۇ باھالاشلارنى تەكشۈرەلمەيمىز + + + ئەپسۇسلىنارلىقى، بىز بەزى تۈردىكى مەھسۇلاتلارنىڭ باھالىنىش سۈپىتىنى تەكشۈرەلمەيمىز. مەسىلەن، سوۋغات كارتىسى ۋە ئاقما سىن، مۇزىكا ۋە ئويۇنلار. + + ئۇچۇر پات يېقىندا كېلىدۇ + + بىز 24 سائەت ئىچىدە بۇ مەھسۇلاتنىڭ باھالىنىشىغا مۇناسىۋەتلىك ئۇچۇرىغا ئېرىشىمىز. قايتا كېلىپ تەكشۈرۈپ بېقىڭ. + + ئەڭ يېڭى تەھلىل + + بىلدىم + + ھازىرچە ھېچقانداق ئۇچۇر يوق + + بىز بۇ مەسىلىنى ھەل قىلىش ئۈچۈن تىرىشىۋاتىمىز. پات پۇرسەتتە قايتا تەكشۈرۈپ بېقىڭ. + + تور باغلىنىشى يوق + + تور باغلىنىشىڭىزنى تەكشۈرۈپ ئاندىن بەتنى قايتا يۈكلەڭ. + + بۇ باھالاش توغرىسىدا ھازىرچە ھېچقانداق ئۇچۇر يوق + + بۇ مەھسۇلاتنىڭ باھالىنىشىنىڭ ئىشەنچلىك ياكى ئەمەسلىكىنى بىلىش ئۈچۈن، باھالىنىش سۈپىتىنى تەكشۈرۈڭ. بۇنىڭغا پەقەت 60 سېكۇنتلا ۋاقىت كېتىدۇ. + + باھالاش سۈپىتىنى تەكشۈر + + + مەھسۇلات باھالىنىشغا قارىتا ئىشەنچلىك يېتەكچىمىزنى سىناپ بېقىڭ + + تەپسىلاتى + + «ھەئە، سىناپ باقاي» تاللانسا سىز Mozilla نىڭ %2$s ۋە %3$s دىكى %1$s غا قوشۇلغان بولىسىز. + + «ھەئە، سىناپ باقاي» تاللانسا تۆۋەندىكى %1$s غا قوشۇلغان بولىسىز: + + شەخسىيەت تۈزۈمى + + شەخسىيەت تۈزۈمى + + ئىشلىتىش شەرتلىرى + + ئىشلىتىش شەرتلىرى + + ھەئە، سىناپ باقاي + + ھازىر ئەمەس + + سېتىۋېلىشتىن بۇرۇن بۇ مەھسۇلاتنىڭ باھالىنىشىنىڭ ئىشەنچلىك ياكى ئەمەسلىكىنى بىلىپ بېقىڭ. + + باھالاش تەكشۈرگۈچنى سىناي + + + بۇ باھالىنىش ئىشەنچلىكمۇ؟ تەڭشەلگەن باھانى كۆرۈپ بېقىڭ. + + باھالاش تەكشۈرگۈچنى ئاچ + + سىناق + + باھالاش تەكشۈرگۈچنى ئاچ + + باھالاش تەكشۈرگۈچنى تاقا + + %1$s يۇلتۇز (يۇقىرىسى 5 يۇلتۇز ) + + ئازراق كۆرسەت + + كۆپرەك كۆرسەت + + سۈپەت + + باھاسى + + توشۇش + + ئورالمىسى ۋە كۆرۈنۈشى + + رىقابەت كۈچى + + + + يىغ + + يىغىلدى + + ياي + + يېيىلدى + + بۇ يىغقۇچ ھەققىدە تېخىمۇ كۆپ ئۇچۇرنى بىلمەكچى بولسىڭىز ئۇلانمىنى ئېچىڭ + + ماقالىنى ئوقۇڭ + + تەپسىلاتى ئۈچۈن ئۇلانما ئېچىلىدۇ diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 7c0a50b086..eb7b0df7f3 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -525,6 +525,11 @@ Дозволити + + %1$s щойно відхилив куки для вас + + Менше відволікань і менше кук, які відстежують вас на цьому сайті. + Намагатися автоматично доступатися до сайтів за допомогою протоколу шифрування HTTPS для поліпшення безпеки. @@ -637,6 +642,8 @@ Додатки + + Встановити додаток з файлу Сповіщення @@ -2250,8 +2257,6 @@ Реклама від %s - Засіб перевірки відгуків розроблено %s. - Засіб перевірки відгуків від %s %s від Mozilla @@ -2311,6 +2316,8 @@ Спробуйте наш надійний путівник відгуками про товари Дізнайтеся чи надійні відгуки про товар, перед купівлею в %1$s. Перевірка відгуків — вбудована у браузер експериментальна функція від %2$s. Вона також працює на %3$s і %4$s. + + Дізнайтеся чи надійні відгуки про товар, перед купівлею в %1$s. Перевірка відгуків – вбудована у браузер експериментальна функція від %2$s. Використовуючи можливості %1$s від Mozilla, ми допомагаємо вам уникнути упереджених і несправжніх відгуків. Наша модель ШІ постійно вдосконалюється, щоб захистити вас під час покупок. %2$s @@ -2378,4 +2385,6 @@ читати статтю відкрити посилання, щоб дізнатися більше + + %s, заголовок diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 29d737c6ca..cc346e7c95 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -520,6 +520,11 @@ Cho phép + + %1$s vừa từ chối cookie cho bạn + + Ít phiền nhiễu hơn, ít cookie theo dõi bạn trên trang web này hơn. + Tự động cố gắng kết nối với các trang web bằng giao thức mã hóa HTTPS để tăng cường bảo mật. @@ -629,6 +634,8 @@ Tiện ích + + Cài đặt tiện ích từ tập tin Thông báo @@ -2227,8 +2234,6 @@ Quảng cáo bởi %s - Trình kiểm tra đánh giá được cung cấp bởi %s. - Trình kiểm tra đánh giá được cung cấp bởi %s %s bởi Mozilla @@ -2288,6 +2293,8 @@ Hãy thử hướng dẫn đáng tin cậy của chúng tôi để đánh giá sản phẩm Xem đánh giá sản phẩm đáng tin cậy trên %1$s trước khi bạn mua. Trình kiểm tra đánh giá, một tính năng thử nghiệm từ %2$s, được tích hợp ngay trong trình duyệt. Nó cũng hoạt động trên %3$s và %4$s. + + Xem đánh giá sản phẩm đáng tin cậy trên %1$strước khi bạn mua. Trình kiểm tra đánh giá, một tính năng thử nghiệm từ %2$s, được tích hợp ngay trong trình duyệt. Sử dụng %1$s bởi Mozilla, chúng tôi giúp bạn tránh những đánh giá thiên vị và không xác thực. Mô hình AI của chúng tôi luôn cải tiến để bảo vệ bạn khi bạn mua sắm. %2$s @@ -2355,4 +2362,6 @@ đọc bài viết mở liên kết để tìm hiểu thêm + + %s, Tiêu đề diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 5cda24da53..5999199bdb 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -473,16 +473,20 @@ HTTPS-Only 模式 - 减少 Cookie 横幅 + 减少 Cookie 横幅 + + Cookie 横幅拦截器 + + 隐私浏览中的 Cookie 横幅拦截器 - 减少 Cookie 横幅 + 减少 Cookie 横幅 - 关闭 + 关闭 - 开启 + 开启 - %1$s 会尝试自动拒绝 Cookie 横幅的 Cookie 请求。 + %1$s 会尝试自动拒绝 Cookie 横幅的 Cookie 请求。 在该网站关闭 @@ -500,17 +504,25 @@ 目前不支持该网站 - 要为 %1$s 开启“减少 Cookie 横幅”功能吗? + 要为 %1$s 开启“减少 Cookie 横幅”功能吗? + + 要为 %1$s 开启 Cookie 横幅拦截器吗? - 要为 %1$s 关闭“减少 Cookie 横幅”功能吗? + 要为 %1$s 关闭“减少 Cookie 横幅”功能吗? + + 要为 %1$s 关闭 Cookie 横幅拦截器吗? %1$s 无法自动拒绝此站点上的 Cookie 请求。您可以请求将来对此站点的支持。 - %1$s 将清除此网站的 Cookie 并刷新页面。清除 Cookie 可能会导致您退出登录,或清空购物车。 + %1$s 将清除此网站的 Cookie 并刷新页面。清除 Cookie 可能会导致您退出登录,或清空购物车。 + + 关闭后,%1$s 将清除 Cookie 并重新加载此网站。这可能导致退出登录或清空购物车。 - %1$s 会尝试在支持的网站上尽可能拒绝所有 Cookie 请求。 + %1$s 会尝试在支持的网站上尽可能拒绝所有 Cookie 请求。 + + 开启后,%1$s 将尝试自动拒绝此网站上的 Cookie 横幅。 要允许 %1$s 拒绝 Cookie 横幅吗? @@ -523,6 +535,11 @@ 允许 + + %1$s 刚刚为您拒绝了 Cookie 请求 + + 在此网站上专注浏览,少受 Cookie 跟踪。 + 自动尝试使用 HTTPS 加密协议连接至网站,增强安全性。 @@ -633,6 +650,8 @@ 附加组件 + + 从文件安装附加组件 通知 @@ -2232,7 +2251,7 @@ 调整后的评分 - 已移除不可信的评价 + 已排除不可信的评价 最有帮助的近期评价 @@ -2280,8 +2299,6 @@ 由 %s 提供的广告 - - 核查评价由 %s 提供支持。 核查评价由 %s 提供支持 @@ -2347,6 +2364,8 @@ 试试我们可信赖的商品评价向导 决定购买前,先看看 %1$s 上的商品评价是否可信。核查评价是 %2$s 的一项实验功能,内置于浏览器中。%3$s、%4$s 上也能用。 + + 决定购买前,先看看 %1$s 上的商品评价是否可信。核查评价是 %2$s 的一项实验功能,内置于浏览器中。 由 Mozilla 支持的 %1$s 可帮助您规避不真实、不公允的评价。在您购物时,我们的人工智能模型也会不断改进,以更好地保护您的权益。%2$s @@ -2448,4 +2467,6 @@ 您确定要删除附加组件元数据缓存文件吗? 确定 取消 + + %s,标题 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index be0bc16542..102e49064c 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -529,6 +529,11 @@ 允許 + + %1$s 剛為您拒絕了 Cookie + + 少一分會追蹤您的 Cookie,也讓您更不用分心。 + 自動嘗試使用加密過的 HTTPS 通訊協定連線到網站,以增加安全性。 @@ -640,6 +645,8 @@ 附加元件 + + 從檔案安裝附加元件 通知 @@ -1701,7 +1708,7 @@ 程式錯誤 - 隱私權保護政策 + 隱私權公告 了解您的權利 @@ -2274,8 +2281,6 @@ 由 %s 提供的廣告 - 商品評論檢查器是由 %s 提供。 - 商品評論檢查器是由 %s 提供 %s by Mozilla @@ -2335,6 +2340,8 @@ 試用我們可信任的商品評論 購買商品之前,先看看 %1$s 上的商品評論可不可靠。%2$s 的實驗性功能「商品評論檢查器」直接內建於瀏覽器中。此功能也可用於 %3$s 及 %4$s 上的商品評論。 + + 購買商品之前,先看看 %1$s 上的商品評論可不可靠。%2$s 的實驗性功能「商品評論檢查器」直接內建於瀏覽器中。 透過 %1$s by Mozilla 的威力,我們協助您避免帶有偏見與虛偽的評論。我們會隨您在線上購物,持續改進 AI 模型。%2$s @@ -2434,4 +2441,6 @@ 您確定要刪除附加元件元數據緩存檔案嗎? 確定 取消 + + 標題 %s diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index f65a56c8ef..8ceff53ac0 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -64,8 +64,6 @@ - - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index b0eaaa3bb3..c48be70a5c 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -93,11 +93,6 @@ 56dp 8dp 8dp - 40dp - 20dp - 16dp - 4dp - 8dp 15sp diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index 47dfe0e4c1..9862c1c2b6 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -161,12 +161,6 @@ pref_key_https_only_in_private_tabs - pref_key_cookie_banner_settings - pref_key_cookie_banner_re_engage_dialog_last_interaction_with_dialog_in_ms - pref_key_cookie_banner_re_engage_dialog_dismissed - pref_key_cookie_banner_first_banner_detected - pref_key_cookie_banner_v1 - pref_key_cookie_banner_re_engagement_dialog_shows_counter pref_key_cookie_banner_private_mode @@ -281,6 +275,8 @@ pref_key_should_show_total_cookie_protection_popup pref_key_should_show_erase_action_popup + + pref_key_should_show_cookie_banners_action_popup pref_key_debug_settings diff --git a/app/src/main/res/values/static_strings.xml b/app/src/main/res/values/static_strings.xml index 2c46623fa8..ed34355757 100644 --- a/app/src/main/res/values/static_strings.xml +++ b/app/src/main/res/values/static_strings.xml @@ -122,4 +122,24 @@ Translate to Not now Translate + + Translation Options + Always offer to translate + Always translate %1$s + Never translate %1$s + Never translate this site + Translation settings + About translations in %1$s + + Translations + Translation preferences + Offer to translate when possible + Always download languages in data saving mode + Automatic translation + Never translate these sites + Download languages + + In progress + Translating in Progress + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0650587cd8..07e77f8037 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -199,6 +199,8 @@ Library Desktop site + + Open in regular tab Add to Home screen @@ -252,7 +254,7 @@ Scan - Search engine + Search engine Search engine settings @@ -439,15 +441,19 @@ HTTPS-Only Mode - Cookie banner reduction + Cookie banner reduction + + Cookie Banner Blocker + + Cookie Banner Blocker in private browsing - Reduce cookie banners + Reduce cookie banners - Off + Off - On + On - %1$s automatically tries to reject cookie requests on cookie banners. + %1$s automatically tries to reject cookie requests on cookie banners. Off for this site @@ -465,25 +471,37 @@ Site currently not supported - Turn on Cookie banner reduction for %1$s? + Turn on Cookie banner reduction for %1$s? + + Turn on Cookie Banner Blocker for %1$s? + + Turn off Cookie banner reduction for %1$s? - Turn off Cookie banner reduction for %1$s? + Turn off Cookie Banner Blocker for %1$s? %1$s can’t automatically reject cookie requests on this site. You can send a request to support this site in the future. - %1$s will clear this site’s cookies and refresh the page. Clearing all cookies may sign you out or empty shopping carts. + %1$s will clear this site’s cookies and refresh the page. Clearing all cookies may sign you out or empty shopping carts. + + Turn off and %1$s will clear cookies and reload this site. This may sign you out or empty shopping carts. - %1$s tries to automatically reject all cookie requests on supported sites. + %1$s tries to automatically reject all cookie requests on supported sites. + + Turn on and %1$s will try to automatically refuse all cookie banners on this site. - Allow %1$s to reject cookie banners? + Allow %1$s to reject cookie banners? - %1$s can automatically reject many cookie banner requests. + %1$s can automatically reject many cookie banner requests. - Not Now + Not Now - You’ll see fewer cookie requests + You’ll see fewer cookie requests - Allow + Allow + + %1$s just refused cookies for you + + Less distractions, less cookies tracking you on this site. Automatically attempts to connect to sites using HTTPS encryption protocol for increased security. @@ -649,16 +667,10 @@ Learn more Classic %s - - Limited Edition Artist series - - The new Independent Voices collection. %s The Independent Voices collection. %s - - The new Independent Voices collection. The Independent Voices collection. @@ -1395,14 +1407,11 @@ %d tabs - Browsing history and site data Browsing history %d addresses - - Cookies Cookies and site data @@ -1458,61 +1467,10 @@ Group deleted - - Welcome to a better internet - - A browser built for people, not profits. - - Pick up where you left off - - Sync tabs and passwords across devices for seamless screen-switching. - - Sign in Sync is on - - Privacy protection by default - - %1$s automatically stops companies from secretly following you around the web. - - Featuring Total Cookie Protection to stop trackers from using cookies to stalk you across sites. - - Standard (default) - - Balanced for privacy and performance. Pages load normally. - - Strict - - Blocks more trackers so pages load faster, but some on-page functionality may break. - - Pick your toolbar placement - - Keep it on the bottom, or move it to the top. - - You control your data - - Firefox gives you control over what you share online and what you share with us. - - Read our privacy notice - - Ready to open up an amazing internet? - - Start browsing - - Choose your theme - - Save some battery and your eyesight with dark mode. - - Automatic - - Adapts to your device settings - - Dark theme - - Light theme - Tabs sent! @@ -1938,8 +1896,6 @@ Search suggestion API URL - Replace query with “%s”. Example:\nhttp://suggestqueries.google.com/complete/search?client=firefox&q=%s - Replace query with “%s”. Example:\nhttps://suggestqueries.google.com/complete/search?client=firefox&q=%s Save @@ -2273,10 +2229,8 @@ Learn more By selecting “Yes, try it” you agree to %1$s by Mozilla’s %2$s and %3$s. - - By selecting “Yes, try it” you agree to %1$s’s %2$s and %3$s. - By selecting \"Yes, try it\" you agree to the following from %1$s: + By selecting “Yes, try it” you agree to the following from %1$s: privacy policy @@ -2323,8 +2277,12 @@ collapse + + collapsed expand + + expanded open link to learn more about this collection diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 629e9227a5..5f1092c365 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -129,8 +129,8 @@ @color/fx_mobile_layer_color_1 @drawable/home_bottom_bar_background @drawable/home_bottom_bar_background_top - @android:color/transparent - @color/fx_mobile_text_color_primary + @android:color/transparent + @color/fx_mobile_text_color_primary ?attr/textPrimary @@ -320,8 +320,8 @@ @drawable/private_home_background_gradient @drawable/private_home_bottom_bar_background_gradient @drawable/private_home_bottom_bar_background_gradient_top - @color/fx_mobile_private_text_color_primary - @color/fx_mobile_private_layer_color_1 + @color/photonWhite + @color/fx_mobile_private_layer_color_2 ?attr/textPrimary @@ -381,26 +381,6 @@ ?textActionPrimary - -