summaryrefslogtreecommitdiffstats
path: root/browser/components/shopping/tests
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/shopping/tests')
-rw-r--r--browser/components/shopping/tests/browser/browser.toml79
-rw-r--r--browser/components/shopping/tests/browser/browser_adjusted_rating.js115
-rw-r--r--browser/components/shopping/tests/browser/browser_ads_exposure_telemetry.js213
-rw-r--r--browser/components/shopping/tests/browser/browser_analysis_explainer.js43
-rw-r--r--browser/components/shopping/tests/browser/browser_auto_open.js90
-rw-r--r--browser/components/shopping/tests/browser/browser_exposure_telemetry.js123
-rw-r--r--browser/components/shopping/tests/browser/browser_inprogress_analysis.js151
-rw-r--r--browser/components/shopping/tests/browser/browser_keep_close_message_bar.js530
-rw-r--r--browser/components/shopping/tests/browser/browser_network_offline.js33
-rw-r--r--browser/components/shopping/tests/browser/browser_not_enough_reviews.js80
-rw-r--r--browser/components/shopping/tests/browser/browser_page_not_supported.js36
-rw-r--r--browser/components/shopping/tests/browser/browser_private_mode.js35
-rw-r--r--browser/components/shopping/tests/browser/browser_recommended_ad_test.js71
-rw-r--r--browser/components/shopping/tests/browser/browser_review_highlights.js194
-rw-r--r--browser/components/shopping/tests/browser/browser_settings_telemetry.js102
-rw-r--r--browser/components/shopping/tests/browser/browser_shopping_card.js50
-rw-r--r--browser/components/shopping/tests/browser/browser_shopping_container.js41
-rw-r--r--browser/components/shopping/tests/browser/browser_shopping_message_triggers.js315
-rw-r--r--browser/components/shopping/tests/browser/browser_shopping_onboarding.js661
-rw-r--r--browser/components/shopping/tests/browser/browser_shopping_settings.js642
-rw-r--r--browser/components/shopping/tests/browser/browser_shopping_sidebar.js66
-rw-r--r--browser/components/shopping/tests/browser/browser_shopping_survey.js337
-rw-r--r--browser/components/shopping/tests/browser/browser_shopping_urlbar.js427
-rw-r--r--browser/components/shopping/tests/browser/browser_stale_product.js36
-rw-r--r--browser/components/shopping/tests/browser/browser_ui_telemetry.js762
-rw-r--r--browser/components/shopping/tests/browser/browser_unanalyzed_product.js86
-rw-r--r--browser/components/shopping/tests/browser/browser_unavailable_product.js102
-rw-r--r--browser/components/shopping/tests/browser/head.js225
28 files changed, 5645 insertions, 0 deletions
diff --git a/browser/components/shopping/tests/browser/browser.toml b/browser/components/shopping/tests/browser/browser.toml
new file mode 100644
index 0000000000..d93abec789
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser.toml
@@ -0,0 +1,79 @@
+[DEFAULT]
+support-files = [
+ "head.js",
+ "!/toolkit/components/shopping/test/mockapis/server_helper.js",
+ "!/toolkit/components/shopping/test/mockapis/analysis_status.sjs",
+ "!/toolkit/components/shopping/test/mockapis/analysis.sjs",
+ "!/toolkit/components/shopping/test/mockapis/analyze.sjs",
+ "!/toolkit/components/shopping/test/mockapis/attribution.sjs",
+ "!/toolkit/components/shopping/test/mockapis/recommendations.sjs",
+ "!/toolkit/components/shopping/test/mockapis/reporting.sjs",
+]
+
+prefs = [
+ "browser.shopping.experience2023.enabled=true",
+ "browser.shopping.experience2023.optedIn=1",
+ "browser.shopping.experience2023.ads.enabled=true",
+ "browser.shopping.experience2023.ads.userEnabled=true",
+ "browser.shopping.experience2023.autoOpen.enabled=false",
+ "browser.shopping.experience2023.autoOpen.userEnabled=true",
+ "toolkit.shopping.environment=test",
+ "toolkit.shopping.ohttpRelayURL=https://example.com/relay", # These URLs don't actually host a relay or gateway config, but are needed to stop us making outside network connections.
+ "toolkit.shopping.ohttpConfigURL=https://example.com/ohttp-config",
+ "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features=false", # Disable the fakespot feature callouts to avoid interference. Individual tests that need them can re-enable them as needed.
+]
+
+["browser_adjusted_rating.js"]
+
+["browser_ads_exposure_telemetry.js"]
+
+["browser_analysis_explainer.js"]
+
+["browser_auto_open.js"]
+
+["browser_exposure_telemetry.js"]
+
+["browser_inprogress_analysis.js"]
+
+["browser_keep_close_message_bar.js"]
+
+["browser_network_offline.js"]
+
+["browser_not_enough_reviews.js"]
+
+["browser_page_not_supported.js"]
+
+["browser_private_mode.js"]
+
+["browser_recommended_ad_test.js"]
+
+["browser_review_highlights.js"]
+
+["browser_settings_telemetry.js"]
+
+["browser_shopping_card.js"]
+
+["browser_shopping_container.js"]
+
+["browser_shopping_message_triggers.js"]
+
+["browser_shopping_onboarding.js"]
+
+["browser_shopping_settings.js"]
+
+["browser_shopping_sidebar.js"]
+
+["browser_shopping_survey.js"]
+
+["browser_shopping_urlbar.js"]
+
+["browser_stale_product.js"]
+
+["browser_ui_telemetry.js"]
+skip-if = [
+ "os == 'linux' && os_version == '18.04'"
+]
+
+["browser_unanalyzed_product.js"]
+
+["browser_unavailable_product.js"]
diff --git a/browser/components/shopping/tests/browser/browser_adjusted_rating.js b/browser/components/shopping/tests/browser/browser_adjusted_rating.js
new file mode 100644
index 0000000000..b0d2da41d5
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_adjusted_rating.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_adjusted_rating() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [MOCK_ANALYZED_PRODUCT_RESPONSE],
+ async mockData => {
+ let rating = mockData.adjusted_rating;
+
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+
+ let adjustedRating = shoppingContainer.adjustedRatingEl;
+ await adjustedRating.updateComplete;
+
+ let mozFiveStar = adjustedRating.ratingEl;
+ ok(mozFiveStar, "The moz-five-star element exists");
+
+ is(
+ mozFiveStar.rating,
+ rating,
+ `The moz-five-star rating is ${rating}`
+ );
+ is(
+ adjustedRating.rating,
+ rating,
+ `The adjusted rating "rating" is ${rating}`
+ );
+
+ rating = 2.55;
+ adjustedRating.rating = rating;
+
+ await adjustedRating.updateComplete;
+
+ is(
+ mozFiveStar.rating,
+ rating,
+ `The moz-five-star rating is now ${rating}`
+ );
+ is(
+ adjustedRating.rating,
+ rating,
+ `The adjusted rating "rating" is now ${rating}`
+ );
+
+ rating = 0;
+ adjustedRating.rating = rating;
+
+ await adjustedRating.updateComplete;
+
+ is(
+ adjustedRating.rating,
+ rating,
+ `The adjusted rating "rating" is now ${rating}`
+ );
+
+ is(
+ mozFiveStar.rating,
+ 0.5,
+ `When the rating is 0, the star rating displays 0.5 stars.`
+ );
+
+ rating = null;
+ adjustedRating.rating = rating;
+
+ await adjustedRating.updateComplete;
+
+ is(
+ adjustedRating.rating,
+ rating,
+ `The adjusted rating "rating" is now ${rating}`
+ );
+
+ ok(
+ ContentTaskUtils.isHidden(adjustedRating),
+ "adjusted rating should not be visible"
+ );
+
+ rating = 3;
+ adjustedRating.rating = rating;
+
+ await adjustedRating.updateComplete;
+ mozFiveStar = adjustedRating.ratingEl;
+ ok(
+ ContentTaskUtils.isVisible(adjustedRating),
+ "adjusted rating should be visible"
+ );
+ is(
+ mozFiveStar.rating,
+ rating,
+ `The moz-five-star rating is now ${rating}`
+ );
+ is(
+ adjustedRating.rating,
+ rating,
+ `The adjusted rating "rating" is now ${rating}`
+ );
+ }
+ );
+ }
+ );
+});
diff --git a/browser/components/shopping/tests/browser/browser_ads_exposure_telemetry.js b/browser/components/shopping/tests/browser/browser_ads_exposure_telemetry.js
new file mode 100644
index 0000000000..5358667716
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_ads_exposure_telemetry.js
@@ -0,0 +1,213 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PRODUCT_PAGE = "https://example.com/product/B09TJGHL5F";
+
+const ADS_JSON = `[{
+ "name": "Test product name ftw",
+ "url": ${PRODUCT_PAGE},
+ "image_url": "https://i.fakespot.io/b6vx27xf3rgwr1a597q6qd3rutp6",
+ "price": "249.99",
+ "currency": "USD",
+ "grade": "A",
+ "adjusted_rating": 4.6,
+ "analysis_url": "https://www.fakespot.com/product/test-product",
+ "sponsored": true,
+ "aid": "a2VlcCBvbiByb2NraW4gdGhlIGZyZWUgd2ViIQ==",
+}]`;
+
+// Verifies that, if the ads server returns an ad, but we have disabled
+// ads exposure, no Glean telemetry is recorded.
+add_task(async function test_ads_exposure_disabled_not_recorded() {
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.ads.enabled", false],
+ ["browser.shopping.experience2023.ads.exposure", false],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [PRODUCT_PAGE, ADS_JSON],
+ async (prodPage, adResponse) => {
+ const { ShoppingProduct } = ChromeUtils.importESModule(
+ "chrome://global/content/shopping/ShoppingProduct.mjs"
+ );
+ const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+ );
+
+ let productURI = Services.io.newURI(prodPage);
+ let product = new ShoppingProduct(productURI);
+ let productRequestAdsStub = sinon.stub(
+ product,
+ "requestRecommendations"
+ );
+ productRequestAdsStub.resolves(adResponse);
+
+ let actor = content.windowGlobalChild.getActor("ShoppingSidebar");
+ actor.productURI = productURI;
+ actor.product = product;
+
+ actor.requestRecommendations(productURI);
+
+ Assert.ok(
+ productRequestAdsStub.notCalled,
+ "product.requestRecommendations should not have been called if ads and ads exposure were disabled"
+ );
+ }
+ );
+ }
+ );
+
+ await Services.fog.testFlushAllChildren();
+
+ var events = Glean.shopping.adsExposure.testGetValue();
+ Assert.equal(events, null, "Ads exposure should not have been recorded");
+ await SpecialPowers.popPrefEnv();
+});
+
+// Verifies that, if the ads server returns nothing, and ads exposure is
+// enabled, no Glean telemetry is recorded.
+add_task(async function test_ads_exposure_enabled_no_ad_not_recorded() {
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.ads.enabled", true],
+ ["browser.shopping.experience2023.ads.exposure", true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(browser, [PRODUCT_PAGE], async prodPage => {
+ const { ShoppingProduct } = ChromeUtils.importESModule(
+ "chrome://global/content/shopping/ShoppingProduct.mjs"
+ );
+ const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+ );
+
+ let productURI = Services.io.newURI(prodPage);
+ let product = new ShoppingProduct(productURI);
+ let productRequestAdsStub = sinon.stub(
+ product,
+ "requestRecommendations"
+ );
+ productRequestAdsStub.resolves([]);
+
+ let actor = content.windowGlobalChild.getActor("ShoppingSidebar");
+ actor.productURI = productURI;
+ actor.product = product;
+
+ actor.requestRecommendations(productURI);
+
+ Assert.ok(
+ productRequestAdsStub.called,
+ "product.requestRecommendations should have been called"
+ );
+ });
+ }
+ );
+
+ await Services.fog.testFlushAllChildren();
+
+ var events = Glean.shopping.adsExposure.testGetValue();
+ Assert.equal(
+ events,
+ null,
+ "Ads exposure should not have been recorded if ads exposure was enabled but no ads were returned"
+ );
+ await SpecialPowers.popPrefEnv();
+});
+
+// Verifies that, if ads are disabled but ads exposure is enabled, ads will
+// be fetched, and if an ad is returned, the Glean probe will be recorded.
+add_task(async function test_ads_exposure_enabled_with_ad_recorded() {
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.ads.enabled", false],
+ ["browser.shopping.experience2023.ads.exposure", true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [PRODUCT_PAGE, ADS_JSON],
+ async (prodPage, adResponse) => {
+ const { ShoppingProduct } = ChromeUtils.importESModule(
+ "chrome://global/content/shopping/ShoppingProduct.mjs"
+ );
+ const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+ );
+
+ let productURI = Services.io.newURI(prodPage);
+ let product = new ShoppingProduct(productURI);
+ let productRequestAdsStub = sinon.stub(
+ product,
+ "requestRecommendations"
+ );
+ productRequestAdsStub.resolves(adResponse);
+
+ let actor = content.windowGlobalChild.getActor("ShoppingSidebar");
+ actor.productURI = productURI;
+ actor.product = product;
+
+ actor.requestRecommendations(productURI);
+
+ Assert.ok(
+ productRequestAdsStub.called,
+ "product.requestRecommendations should have been called if ads exposure is enabled, even if ads are not"
+ );
+ }
+ );
+ }
+ );
+
+ await Services.fog.testFlushAllChildren();
+
+ const events = Glean.shopping.adsExposure.testGetValue();
+ Assert.equal(
+ events.length,
+ 1,
+ "Ads exposure should have been recorded if ads exposure was enabled and ads were returned"
+ );
+ Assert.equal(
+ events[0].category,
+ "shopping",
+ "Glean event should have category 'shopping'"
+ );
+ Assert.equal(
+ events[0].name,
+ "ads_exposure",
+ "Glean event should have name 'ads_exposure'"
+ );
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/shopping/tests/browser/browser_analysis_explainer.js b/browser/components/shopping/tests/browser/browser_analysis_explainer.js
new file mode 100644
index 0000000000..cb73a80709
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_analysis_explainer.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the analysis explainer SUMO link is rendered with the expected
+ * UTM parameters.
+ */
+add_task(async function test_analysis_explainer_sumo_link_utm() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [MOCK_ANALYZED_PRODUCT_RESPONSE],
+ async mockData => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+
+ let card =
+ shoppingContainer.analysisExplainerEl.shadowRoot.querySelector(
+ "shopping-card"
+ );
+
+ let href = card.querySelector("a").href;
+ let qs = new URL(href).searchParams;
+ is(qs.get("as"), "u");
+ is(qs.get("utm_source"), "inproduct");
+ is(qs.get("utm_campaign"), "learn-more");
+ is(qs.get("utm_term"), "core-sidebar");
+ }
+ );
+ }
+ );
+});
diff --git a/browser/components/shopping/tests/browser/browser_auto_open.js b/browser/components/shopping/tests/browser/browser_auto_open.js
new file mode 100644
index 0000000000..69c37316c5
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_auto_open.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ShoppingUtils: "resource:///modules/ShoppingUtils.sys.mjs",
+});
+
+const ACTIVE_PREF = "browser.shopping.experience2023.active";
+const AUTO_OPEN_ENABLED_PREF =
+ "browser.shopping.experience2023.autoOpen.enabled";
+const AUTO_OPEN_USER_ENABLED_PREF =
+ "browser.shopping.experience2023.autoOpen.userEnabled";
+const PRODUCT_PAGE = "https://example.com/product/B09TJGHL5F";
+const productURI = Services.io.newURI(PRODUCT_PAGE);
+
+async function trigger_auto_open_flow(expectedActivePrefValue) {
+ // Set the active pref to false, which triggers ShoppingUtils.onActiveUpdate.
+ Services.prefs.setBoolPref(ACTIVE_PREF, false);
+
+ // Call onLocationChange with a product URL, triggering auto-open to flip the
+ // active pref back to true, if the auto-open conditions are satisfied.
+ ShoppingUtils.onLocationChange(productURI, 0);
+
+ // Wait a turn for the change to propagate...
+ await TestUtils.waitForTick();
+
+ // Finally, assert the active pref has the expected state.
+ Assert.equal(
+ expectedActivePrefValue,
+ Services.prefs.getBoolPref(ACTIVE_PREF, false)
+ );
+}
+
+add_task(async function test_auto_open() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.optedIn", 1],
+ ["browser.shopping.experience2023.autoOpen.enabled", true],
+ ["browser.shopping.experience2023.autoOpen.userEnabled", true],
+ ],
+ });
+
+ await trigger_auto_open_flow(true);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_auto_open_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.optedIn", 1],
+ ["browser.shopping.experience2023.autoOpen.enabled", false],
+ ["browser.shopping.experience2023.autoOpen.userEnabled", true],
+ ],
+ });
+
+ await trigger_auto_open_flow(false);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_auto_open_user_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.optedIn", 1],
+ ["browser.shopping.experience2023.autoOpen.enabled", true],
+ ["browser.shopping.experience2023.autoOpen.userEnabled", false],
+ ],
+ });
+
+ await trigger_auto_open_flow(false);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_auto_open_not_opted_in() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.optedIn", 0],
+ ["browser.shopping.experience2023.autoOpen.enabled", true],
+ ["browser.shopping.experience2023.autoOpen.userEnabled", true],
+ ],
+ });
+
+ await trigger_auto_open_flow(false);
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/shopping/tests/browser/browser_exposure_telemetry.js b/browser/components/shopping/tests/browser/browser_exposure_telemetry.js
new file mode 100644
index 0000000000..51334ce722
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_exposure_telemetry.js
@@ -0,0 +1,123 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ShoppingUtils: "resource:///modules/ShoppingUtils.sys.mjs",
+});
+
+// Tests in this file simulate exposure detection without actually loading the
+// product pages. Instead, we call the `ShoppingUtils.maybeRecordExposure`
+// method, passing in flags and URLs to simulate onLocationChange events.
+// Bug 1853401 captures followup work to add integration tests.
+
+const PRODUCT_PAGE = Services.io.newURI(
+ "https://example.com/product/B09TJGHL5F"
+);
+const WALMART_PAGE = Services.io.newURI(
+ "https://www.walmart.com/ip/Utz-Cheese-Balls-23-Oz/15543964"
+);
+const WALMART_OTHER_PAGE = Services.io.newURI(
+ "https://www.walmart.com/ip/Utz-Gluten-Free-Cheese-Balls-23-0-OZ/10898644"
+);
+
+async function setup(pref) {
+ await SpecialPowers.pushPrefEnv({
+ set: [[`browser.shopping.experience2023.${pref}`, true]],
+ });
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+}
+
+async function teardown(pref) {
+ await SpecialPowers.popPrefEnv();
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+
+ // Clear out the normally short-lived pushState navigation cache in between
+ // runs, to avoid accidentally deduping when we shouldn't.
+ ShoppingUtils.lastWalmartURI = null;
+}
+
+async function runTest({ aLocationURI, aFlags, expected }) {
+ async function _run() {
+ Assert.equal(undefined, Glean.shopping.productPageVisits.testGetValue());
+ ShoppingUtils.onLocationChange(aLocationURI, aFlags);
+ await Services.fog.testFlushAllChildren();
+ Assert.equal(expected, Glean.shopping.productPageVisits.testGetValue());
+ }
+
+ await setup("enabled");
+ await _run();
+ await teardown("enabled");
+
+ await setup("control");
+ await _run();
+ await teardown("control");
+}
+
+add_task(async function test_shopping_exposure_new_page() {
+ await runTest({
+ aLocationURI: PRODUCT_PAGE,
+ aFlags: 0,
+ expected: 1,
+ });
+});
+
+add_task(async function test_shopping_exposure_reload_page() {
+ await runTest({
+ aLocationURI: PRODUCT_PAGE,
+ aFlags: Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD,
+ expected: 1,
+ });
+});
+
+add_task(async function test_shopping_exposure_session_restore_page() {
+ await runTest({
+ aLocationURI: PRODUCT_PAGE,
+ aFlags: Ci.nsIWebProgressListener.LOCATION_CHANGE_SESSION_STORE,
+ expected: 1,
+ });
+});
+
+add_task(async function test_shopping_exposure_ignore_same_page() {
+ await runTest({
+ aLocationURI: PRODUCT_PAGE,
+ aFlags: Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT,
+ expected: undefined,
+ });
+});
+
+add_task(async function test_shopping_exposure_count_same_page_pushstate() {
+ await runTest({
+ aLocationURI: WALMART_PAGE,
+ aFlags: Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT,
+ expected: 1,
+ });
+});
+
+add_task(async function test_shopping_exposure_ignore_pushstate_repeats() {
+ async function _run() {
+ let aFlags = Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT;
+ Assert.equal(undefined, Glean.shopping.productPageVisits.testGetValue());
+
+ // Slightly different setup here: simulate deduping by setting the first
+ // walmart page's URL as the `ShoppingUtils.lastWalmartURI`, then fire the
+ // pushState for the first page, then twice for a second page. This seems
+ // to be roughly the observed behavior when navigating between walmart
+ // product pages.
+ ShoppingUtils.lastWalmartURI = WALMART_PAGE;
+ ShoppingUtils.onLocationChange(WALMART_PAGE, aFlags);
+ ShoppingUtils.onLocationChange(WALMART_OTHER_PAGE, aFlags);
+ ShoppingUtils.onLocationChange(WALMART_OTHER_PAGE, aFlags);
+ await Services.fog.testFlushAllChildren();
+ Assert.equal(1, Glean.shopping.productPageVisits.testGetValue());
+ }
+ await setup("enabled");
+ await _run();
+ await teardown("enabled");
+ await setup("control");
+ await _run();
+ await teardown("control");
+});
diff --git a/browser/components/shopping/tests/browser/browser_inprogress_analysis.js b/browser/components/shopping/tests/browser/browser_inprogress_analysis.js
new file mode 100644
index 0000000000..d2d1ddeb8c
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_inprogress_analysis.js
@@ -0,0 +1,151 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the correct shopping-message-bar component appears after requesting analysis for an unanalyzed product.
+ */
+add_task(async function test_in_progress_analysis_unanalyzed() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [MOCK_UNANALYZED_PRODUCT_RESPONSE],
+ async mockData => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+
+ let unanalyzedProduct = shoppingContainer.unanalyzedProductEl;
+ let analysisButton = unanalyzedProduct.analysisButtonEl;
+
+ let messageBarVisiblePromise = ContentTaskUtils.waitForCondition(
+ () => {
+ return (
+ !!shoppingContainer.shoppingMessageBarEl &&
+ ContentTaskUtils.isVisible(
+ shoppingContainer.shoppingMessageBarEl
+ )
+ );
+ },
+ "Waiting for shopping-message-bar to be visible"
+ );
+
+ analysisButton.click();
+ await shoppingContainer.updateComplete;
+
+ // Mock the response from analysis status being "pending"
+ shoppingContainer.isAnalysisInProgress = true;
+ // Add data back, as it was unset as due to the lack of mock APIs.
+ // TODO: Support for the mocks will be added in Bug 1853474.
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+
+ await messageBarVisiblePromise;
+ await shoppingContainer.updateComplete;
+
+ is(
+ shoppingContainer.shoppingMessageBarEl?.getAttribute("type"),
+ "analysis-in-progress",
+ "shopping-message-bar type should be correct"
+ );
+ }
+ );
+ }
+ );
+});
+
+/**
+ * Tests that the correct shopping-message-bar component appears after re-requesting analysis for a stale product,
+ * and that component shows progress percentage.
+ */
+add_task(async function test_in_progress_analysis_stale() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [MOCK_STALE_PRODUCT_RESPONSE],
+ async mockData => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+
+ let staleMessageBar = shoppingContainer.shoppingMessageBarEl;
+ is(staleMessageBar?.type, "stale", "Got stale message-bar");
+
+ let analysisButton = staleMessageBar.reAnalysisButtonEl;
+
+ let messageBarVisiblePromise = ContentTaskUtils.waitForCondition(
+ () => {
+ return (
+ !!shoppingContainer.shoppingMessageBarEl &&
+ ContentTaskUtils.isVisible(
+ shoppingContainer.shoppingMessageBarEl
+ )
+ );
+ },
+ "Waiting for shopping-message-bar to be visible"
+ );
+
+ analysisButton.click();
+ await shoppingContainer.updateComplete;
+
+ // Mock the response from analysis status being "pending"
+ shoppingContainer.isAnalysisInProgress = true;
+ // Mock the analysis status response with progress.
+ shoppingContainer.analysisProgress = 50;
+ // Add data back, as it was unset as due to the lack of mock APIs.
+ // TODO: Support for the mocks will be added in Bug 1853474.
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+
+ await messageBarVisiblePromise;
+ await shoppingContainer.updateComplete;
+
+ let shoppingMessageBarEl = shoppingContainer.shoppingMessageBarEl;
+ is(
+ shoppingMessageBarEl?.getAttribute("type"),
+ "reanalysis-in-progress",
+ "shopping-message-bar type should be correct"
+ );
+ is(
+ shoppingMessageBarEl?.getAttribute("progress"),
+ "50",
+ "shopping-message-bar should have progress"
+ );
+
+ let messageBarEl =
+ shoppingMessageBarEl?.shadowRoot.querySelector("message-bar");
+ is(
+ messageBarEl?.getAttribute("style"),
+ "--analysis-progress-pcent: 50%;",
+ "message-bar should have progress set as a CSS variable"
+ );
+
+ let messageBarContainerEl =
+ shoppingMessageBarEl?.shadowRoot.querySelector(
+ "#message-bar-container"
+ );
+ is(
+ messageBarContainerEl.querySelector("#header")?.dataset.l10nArgs,
+ `{"percentage":50}`,
+ "message-bar-container header should have progress set as a l10n arg"
+ );
+ }
+ );
+ }
+ );
+});
diff --git a/browser/components/shopping/tests/browser/browser_keep_close_message_bar.js b/browser/components/shopping/tests/browser/browser_keep_close_message_bar.js
new file mode 100644
index 0000000000..c4d5f5f81a
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_keep_close_message_bar.js
@@ -0,0 +1,530 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PRODUCT_PAGE = "https://example.com/product/B09TJGHL5F";
+
+const SIDEBAR_CLOSED_COUNT_PREF =
+ "browser.shopping.experience2023.sidebarClosedCount";
+const SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF =
+ "browser.shopping.experience2023.showKeepSidebarClosedMessage";
+const SHOPPING_SIDEBAR_ACTIVE_PREF = "browser.shopping.experience2023.active";
+const SIDEBAR_AUTO_OPEN_ENABLED_PREF =
+ "browser.shopping.experience2023.autoOpen.enabled";
+const SIDEBAR_AUTO_OPEN_USER_ENABLED_PREF =
+ "browser.shopping.experience2023.autoOpen.userEnabled";
+const SHOPPING_OPTED_IN_PREF = "browser.shopping.experience2023.optedIn";
+
+add_task(
+ async function test_keep_close_message_bar_no_longer_shows_after_3_appearences() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [SHOPPING_SIDEBAR_ACTIVE_PREF, true],
+ [SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF, true],
+ [SHOPPING_OPTED_IN_PREF, 1],
+ [SIDEBAR_CLOSED_COUNT_PREF, 3],
+ [SIDEBAR_AUTO_OPEN_ENABLED_PREF, true],
+ [SIDEBAR_AUTO_OPEN_USER_ENABLED_PREF, true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async browser => {
+ let shoppingButton = document.getElementById("shopping-sidebar-button");
+ let browserPanel = gBrowser.getPanel(browser);
+
+ let sidebar = browserPanel.querySelector("shopping-sidebar");
+
+ function waitForSidebarOpen() {
+ return BrowserTestUtils.waitForMutationCondition(
+ shoppingButton,
+ {
+ attributeFilter: ["shoppingsidebaropen"],
+ },
+ () => shoppingButton.getAttribute("shoppingsidebaropen") === "true"
+ );
+ }
+
+ function waitForSidebarClosed() {
+ return BrowserTestUtils.waitForMutationCondition(
+ shoppingButton,
+ {
+ attributeFilter: ["shoppingsidebaropen"],
+ },
+ () => shoppingButton.getAttribute("shoppingsidebaropen") === "false"
+ );
+ }
+
+ function assertKeepClosedMessageBarVisible() {
+ return SpecialPowers.spawn(
+ sidebar.querySelector("browser"),
+ [],
+ async () => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ await shoppingContainer.updateComplete;
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ !!shoppingContainer.keepClosedMessageBarEl &&
+ ContentTaskUtils.isVisible(
+ shoppingContainer.keepClosedMessageBarEl
+ )
+ );
+ }, "Waiting for keep message bar to be visible");
+
+ await shoppingContainer.keepClosedMessageBarEl.updateComplete;
+
+ Assert.ok(
+ shoppingContainer.showingKeepClosedMessage,
+ "We are showing the keep closed message bar"
+ );
+ }
+ );
+ }
+
+ function assertKeepClosedMessageBarNotShowing() {
+ return SpecialPowers.spawn(
+ sidebar.querySelector("browser"),
+ [],
+ async () => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ await shoppingContainer.updateComplete;
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ !shoppingContainer.keepClosedMessageBarEl ||
+ ContentTaskUtils.isHidden(
+ shoppingContainer.keepClosedMessageBarEl
+ )
+ );
+ }, "Waiting for keep message bar to be visible");
+
+ Assert.ok(
+ !shoppingContainer.showingKeepClosedMessage,
+ "We are not showing the keep closed message bar"
+ );
+ }
+ );
+ }
+
+ function clickSidebarCloseButton() {
+ return SpecialPowers.spawn(
+ sidebar.querySelector("browser"),
+ [],
+ async () => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ await shoppingContainer.updateComplete;
+
+ shoppingContainer.closeButtonEl.click();
+ }
+ );
+ }
+
+ await promiseSidebarUpdated(sidebar, PRODUCT_PAGE);
+
+ await waitForSidebarOpen();
+ ok(
+ BrowserTestUtils.isVisible(sidebar),
+ "Shopping sidebar should be open"
+ );
+
+ // Close sidebar
+ shoppingButton.click();
+
+ await waitForSidebarClosed();
+ ok(
+ BrowserTestUtils.isHidden(sidebar),
+ "Shopping sidebar should be closed"
+ );
+
+ // Open sidebar
+ shoppingButton.click();
+
+ await waitForSidebarOpen();
+ ok(
+ BrowserTestUtils.isVisible(sidebar),
+ "Shopping sidebar should be open"
+ );
+
+ // Try closing sidebar. Keep closed message bar will show
+ await clickSidebarCloseButton();
+
+ await TestUtils.waitForTick();
+ await assertKeepClosedMessageBarVisible();
+
+ await clickSidebarCloseButton();
+
+ await waitForSidebarClosed();
+ ok(
+ BrowserTestUtils.isHidden(sidebar),
+ "Shopping sidebar should be closed"
+ );
+
+ // Open sidebar
+ shoppingButton.click();
+
+ await waitForSidebarOpen();
+ ok(
+ BrowserTestUtils.isVisible(sidebar),
+ "Shopping sidebar should be open"
+ );
+
+ // Try closing sidebar. Keep closed message bar will show
+ shoppingButton.click();
+
+ await TestUtils.waitForTick();
+ await assertKeepClosedMessageBarVisible();
+
+ shoppingButton.click();
+
+ await waitForSidebarClosed();
+ ok(
+ BrowserTestUtils.isHidden(sidebar),
+ "Shopping sidebar should be closed"
+ );
+
+ // Open sidebar
+ shoppingButton.click();
+
+ await waitForSidebarOpen();
+ ok(
+ BrowserTestUtils.isVisible(sidebar),
+ "Shopping sidebar should be open"
+ );
+
+ // Try closing sidebar. Keep closed message bar will show
+ shoppingButton.click();
+
+ await TestUtils.waitForTick();
+ await assertKeepClosedMessageBarVisible();
+
+ shoppingButton.click();
+
+ await waitForSidebarClosed();
+ ok(
+ BrowserTestUtils.isHidden(sidebar),
+ "Shopping sidebar should be closed"
+ );
+
+ // Open sidebar
+ shoppingButton.click();
+
+ await waitForSidebarOpen();
+ ok(
+ BrowserTestUtils.isVisible(sidebar),
+ "Shopping sidebar should be open"
+ );
+ await assertKeepClosedMessageBarNotShowing();
+
+ // Close sidebar. Keep closed message bar no longer shows
+ await clickSidebarCloseButton();
+
+ await waitForSidebarClosed();
+ ok(
+ BrowserTestUtils.isHidden(sidebar),
+ "Shopping sidebar should be closed"
+ );
+ });
+ }
+);
+
+add_task(async function test_keep_close_message_bar_no_thanks() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [SHOPPING_SIDEBAR_ACTIVE_PREF, true],
+ [SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF, true],
+ [SHOPPING_OPTED_IN_PREF, 1],
+ [SIDEBAR_CLOSED_COUNT_PREF, 5],
+ [SIDEBAR_AUTO_OPEN_ENABLED_PREF, true],
+ [SIDEBAR_AUTO_OPEN_USER_ENABLED_PREF, true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async browser => {
+ let shoppingButton = document.getElementById("shopping-sidebar-button");
+ let browserPanel = gBrowser.getPanel(browser);
+
+ let sidebar = browserPanel.querySelector("shopping-sidebar");
+
+ function waitForSidebarOpen() {
+ return BrowserTestUtils.waitForMutationCondition(
+ shoppingButton,
+ {
+ attributeFilter: ["shoppingsidebaropen"],
+ },
+ () => shoppingButton.getAttribute("shoppingsidebaropen") === "true"
+ );
+ }
+
+ function waitForSidebarClosed() {
+ return BrowserTestUtils.waitForMutationCondition(
+ shoppingButton,
+ {
+ attributeFilter: ["shoppingsidebaropen"],
+ },
+ () => shoppingButton.getAttribute("shoppingsidebaropen") === "false"
+ );
+ }
+
+ function assertKeepClosedMessageBarVisible() {
+ return SpecialPowers.spawn(
+ sidebar.querySelector("browser"),
+ [],
+ async () => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ await shoppingContainer.updateComplete;
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ !!shoppingContainer.keepClosedMessageBarEl &&
+ ContentTaskUtils.isVisible(
+ shoppingContainer.keepClosedMessageBarEl
+ )
+ );
+ }, "Waiting for keep message bar to be visible");
+
+ await shoppingContainer.keepClosedMessageBarEl.updateComplete;
+
+ Assert.ok(
+ shoppingContainer.showingKeepClosedMessage,
+ "We are showing the keep closed message bar"
+ );
+ }
+ );
+ }
+
+ function assertKeepClosedMessageBarNotShowing() {
+ return SpecialPowers.spawn(
+ sidebar.querySelector("browser"),
+ [],
+ async () => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ await shoppingContainer.updateComplete;
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ !shoppingContainer.keepClosedMessageBarEl ||
+ ContentTaskUtils.isHidden(
+ shoppingContainer.keepClosedMessageBarEl
+ )
+ );
+ }, "Waiting for keep message bar to be visible");
+
+ Assert.ok(
+ !shoppingContainer.showingKeepClosedMessage,
+ "We are not showing the keep closed message bar"
+ );
+ }
+ );
+ }
+
+ function clickNoThanksButton() {
+ return SpecialPowers.spawn(
+ sidebar.querySelector("browser"),
+ [],
+ async () => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ await shoppingContainer.updateComplete;
+
+ let keepClosedMessageBar = shoppingContainer.keepClosedMessageBarEl;
+ await keepClosedMessageBar.updateComplete;
+
+ keepClosedMessageBar.noThanksButtonEl.click();
+ }
+ );
+ }
+
+ await promiseSidebarUpdated(sidebar, PRODUCT_PAGE);
+
+ await waitForSidebarOpen();
+ ok(BrowserTestUtils.isVisible(sidebar), "Shopping sidebar should be open");
+
+ // Try closing sidebar. Keep closed message bar will show
+ shoppingButton.click();
+
+ await TestUtils.waitForTick();
+ await assertKeepClosedMessageBarVisible();
+
+ await clickNoThanksButton();
+
+ await waitForSidebarClosed();
+ ok(BrowserTestUtils.isHidden(sidebar), "Shopping sidebar should be closed");
+
+ // Open sidebar
+ shoppingButton.click();
+
+ await waitForSidebarOpen();
+ ok(BrowserTestUtils.isVisible(sidebar), "Shopping sidebar should be open");
+ await assertKeepClosedMessageBarNotShowing();
+
+ // Close sidebar. Keep closed message no longer shows
+ shoppingButton.click();
+
+ await waitForSidebarClosed();
+ ok(BrowserTestUtils.isHidden(sidebar), "Shopping sidebar should be closed");
+ });
+});
+
+add_task(async function test_keep_close_message_bar_yes_keep_closed() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [SHOPPING_SIDEBAR_ACTIVE_PREF, true],
+ [SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF, true],
+ [SHOPPING_OPTED_IN_PREF, 1],
+ [SIDEBAR_CLOSED_COUNT_PREF, 5],
+ [SIDEBAR_AUTO_OPEN_ENABLED_PREF, true],
+ [SIDEBAR_AUTO_OPEN_USER_ENABLED_PREF, true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async browser => {
+ let shoppingButton = document.getElementById("shopping-sidebar-button");
+ let browserPanel = gBrowser.getPanel(browser);
+
+ let sidebar = browserPanel.querySelector("shopping-sidebar");
+
+ function waitForSidebarOpen() {
+ return BrowserTestUtils.waitForMutationCondition(
+ shoppingButton,
+ {
+ attributeFilter: ["shoppingsidebaropen"],
+ },
+ () => shoppingButton.getAttribute("shoppingsidebaropen") === "true"
+ );
+ }
+
+ function waitForSidebarClosed() {
+ return BrowserTestUtils.waitForMutationCondition(
+ shoppingButton,
+ {
+ attributeFilter: ["shoppingsidebaropen"],
+ },
+ () => shoppingButton.getAttribute("shoppingsidebaropen") === "false"
+ );
+ }
+
+ function assertKeepClosedMessageBarVisible() {
+ return SpecialPowers.spawn(
+ sidebar.querySelector("browser"),
+ [],
+ async () => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ await shoppingContainer.updateComplete;
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ !!shoppingContainer.keepClosedMessageBarEl &&
+ ContentTaskUtils.isVisible(
+ shoppingContainer.keepClosedMessageBarEl
+ )
+ );
+ }, "Waiting for keep message bar to be visible");
+
+ await shoppingContainer.keepClosedMessageBarEl.updateComplete;
+
+ Assert.ok(
+ shoppingContainer.showingKeepClosedMessage,
+ "We are showing the keep closed message bar"
+ );
+ }
+ );
+ }
+
+ function assertKeepClosedMessageBarNotShowing() {
+ return SpecialPowers.spawn(
+ sidebar.querySelector("browser"),
+ [],
+ async () => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ await shoppingContainer.updateComplete;
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ !shoppingContainer.keepClosedMessageBarEl ||
+ ContentTaskUtils.isHidden(
+ shoppingContainer.keepClosedMessageBarEl
+ )
+ );
+ }, "Waiting for keep message bar to be visible");
+
+ Assert.ok(
+ !shoppingContainer.showingKeepClosedMessage,
+ "We are not showing the keep closed message bar"
+ );
+ }
+ );
+ }
+
+ function clickYesKeepClosedButton() {
+ return SpecialPowers.spawn(
+ sidebar.querySelector("browser"),
+ [],
+ async () => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ await shoppingContainer.updateComplete;
+
+ let keepClosedMessageBar = shoppingContainer.keepClosedMessageBarEl;
+ await keepClosedMessageBar.updateComplete;
+
+ keepClosedMessageBar.yesKeepClosedButtonEl.click();
+ }
+ );
+ }
+
+ await promiseSidebarUpdated(sidebar, PRODUCT_PAGE);
+
+ await waitForSidebarOpen();
+ ok(BrowserTestUtils.isVisible(sidebar), "Shopping sidebar should be open");
+
+ // Try closing sidebar. Keep closed message bar will show
+ shoppingButton.click();
+
+ await TestUtils.waitForTick();
+ await assertKeepClosedMessageBarVisible();
+
+ await clickYesKeepClosedButton();
+
+ await waitForSidebarClosed();
+ ok(BrowserTestUtils.isHidden(sidebar), "Shopping sidebar should be closed");
+
+ // Open sidebar
+ shoppingButton.click();
+
+ await waitForSidebarOpen();
+ ok(BrowserTestUtils.isVisible(sidebar), "Shopping sidebar should be open");
+ await assertKeepClosedMessageBarNotShowing();
+
+ // Close sidebar. Keep closed message no longer shows
+ shoppingButton.click();
+
+ await waitForSidebarClosed();
+ ok(BrowserTestUtils.isHidden(sidebar), "Shopping sidebar should be closed");
+ });
+});
diff --git a/browser/components/shopping/tests/browser/browser_network_offline.js b/browser/components/shopping/tests/browser/browser_network_offline.js
new file mode 100644
index 0000000000..d833d551d8
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_network_offline.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_setup() {
+ let originalIoOffline = Services.io.offline;
+ Services.io.offline = true;
+
+ registerCleanupFunction(() => {
+ Services.io.offline = originalIoOffline;
+ });
+});
+
+/**
+ * Tests that only the loading state appears when there is no network connection.
+ */
+add_task(async function test_offline_warning() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ let shoppingContainer = await getAnalysisDetails(browser, null);
+
+ ok(shoppingContainer.isOffline, "Offline status detected");
+ ok(shoppingContainer.loadingEl, "Render loading state");
+ verifyAnalysisDetailsHidden(shoppingContainer);
+ verifyFooterHidden(shoppingContainer);
+ }
+ );
+});
diff --git a/browser/components/shopping/tests/browser/browser_not_enough_reviews.js b/browser/components/shopping/tests/browser/browser_not_enough_reviews.js
new file mode 100644
index 0000000000..d979156c1e
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_not_enough_reviews.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the unanalyzed card is shown when not_enough_reviews is not present.
+
+add_task(async function test_show_unanalyzed_on_initial_load() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ let shoppingContainer = await getAnalysisDetails(
+ browser,
+ MOCK_UNANALYZED_PRODUCT_RESPONSE
+ );
+ ok(
+ shoppingContainer.unanalyzedProductEl,
+ "Got unanalyzed card on first try"
+ );
+
+ verifyAnalysisDetailsHidden(shoppingContainer);
+ verifyFooterVisible(shoppingContainer);
+ }
+ );
+});
+
+// Tests that the not enough reviews card is shown when not_enough_reviews is true.
+
+add_task(async function test_show_not_enough_reviews() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [MOCK_NOT_ENOUGH_REVIEWS_PRODUCT_RESPONSE],
+ async mockData => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+
+ let messageBarVisiblePromise = ContentTaskUtils.waitForCondition(
+ () => {
+ return (
+ !!shoppingContainer.shoppingMessageBarEl &&
+ ContentTaskUtils.isVisible(
+ shoppingContainer.shoppingMessageBarEl
+ )
+ );
+ },
+ "Waiting for shopping-message-bar to be visible"
+ );
+
+ await messageBarVisiblePromise;
+ await shoppingContainer.updateComplete;
+
+ ok(
+ shoppingContainer.shoppingMessageBarEl,
+ "Got shopping-message-bar element"
+ );
+
+ is(
+ shoppingContainer.shoppingMessageBarEl?.getAttribute("type"),
+ "not-enough-reviews",
+ "shopping-message-bar type should be correct"
+ );
+ }
+ );
+ }
+ );
+});
diff --git a/browser/components/shopping/tests/browser/browser_page_not_supported.js b/browser/components/shopping/tests/browser/browser_page_not_supported.js
new file mode 100644
index 0000000000..c14fd697da
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_page_not_supported.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the correct shopping-message-bar component appears if a page is not supported.
+ * Only footer should be visible.
+ */
+add_task(async function test_page_not_supported() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ let shoppingContainer = await getAnalysisDetails(
+ browser,
+ MOCK_PAGE_NOT_SUPPORTED_RESPONSE
+ );
+
+ ok(
+ shoppingContainer.shoppingMessageBarEl,
+ "Got shopping-message-bar element"
+ );
+ is(
+ shoppingContainer.shoppingMessageBarType,
+ "page-not-supported",
+ "shopping-message-bar type should be correct"
+ );
+
+ verifyAnalysisDetailsHidden(shoppingContainer);
+ verifyFooterVisible(shoppingContainer);
+ }
+ );
+});
diff --git a/browser/components/shopping/tests/browser/browser_private_mode.js b/browser/components/shopping/tests/browser/browser_private_mode.js
new file mode 100644
index 0000000000..16d7ee733b
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_private_mode.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test verifies that the shopping sidebar is not initialized if the
+// user visits a shopping product page while in private browsing mode.
+
+add_task(async function test_private_window_disabled() {
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ let browser = privateWindow.gBrowser.selectedBrowser;
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ "https://example.com/product/B09TJGHL5F"
+ );
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let shoppingButton = privateWindow.document.getElementById(
+ "shopping-sidebar-button"
+ );
+ ok(
+ BrowserTestUtils.isHidden(shoppingButton),
+ "Shopping Button should not be visible on a product page"
+ );
+
+ ok(
+ !privateWindow.document.querySelector("shopping-sidebar"),
+ "Shopping sidebar does not exist"
+ );
+
+ await BrowserTestUtils.closeWindow(privateWindow);
+});
diff --git a/browser/components/shopping/tests/browser/browser_recommended_ad_test.js b/browser/components/shopping/tests/browser/browser_recommended_ad_test.js
new file mode 100644
index 0000000000..159bd0514e
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_recommended_ad_test.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_ads_requested_after_enabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.ads.enabled", true],
+ ["browser.shopping.experience2023.ads.userEnabled", false],
+ ["browser.shopping.experience2023.autoOpen.enabled", true],
+ ["toolkit.shopping.ohttpRelayURL", ""],
+ ["toolkit.shopping.ohttpConfigURL", ""],
+ ],
+ });
+ await BrowserTestUtils.withNewTab(
+ {
+ url: PRODUCT_TEST_URL,
+ gBrowser,
+ },
+ async browser => {
+ let sidebar = gBrowser
+ .getPanel(browser)
+ .querySelector("shopping-sidebar");
+ Assert.ok(sidebar, "Sidebar should exist");
+ Assert.ok(
+ BrowserTestUtils.isVisible(sidebar),
+ "Sidebar should be visible."
+ );
+ info("Waiting for sidebar to update.");
+ await promiseSidebarUpdated(sidebar, PRODUCT_TEST_URL);
+
+ await SpecialPowers.spawn(
+ sidebar.querySelector("browser"),
+ [],
+ async () => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ await shoppingContainer.updateComplete;
+
+ Assert.ok(
+ !shoppingContainer.recommendedAdEl,
+ "Recommended card should not exist"
+ );
+
+ let shoppingSettings = shoppingContainer.settingsEl;
+ await shoppingSettings.updateComplete;
+
+ let recommendationsToggle = shoppingSettings.recommendationsToggleEl;
+ recommendationsToggle.click();
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return shoppingContainer.recommendedAdEl;
+ });
+
+ await shoppingContainer.updateComplete;
+
+ let recommendedCard = shoppingContainer.recommendedAdEl;
+ await recommendedCard.updateComplete;
+ Assert.ok(recommendedCard, "Recommended card should exist");
+ Assert.ok(
+ ContentTaskUtils.isVisible(recommendedCard),
+ "Recommended card is visible"
+ );
+ }
+ );
+ }
+ );
+});
diff --git a/browser/components/shopping/tests/browser/browser_review_highlights.js b/browser/components/shopping/tests/browser/browser_review_highlights.js
new file mode 100644
index 0000000000..f4f3467a80
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_review_highlights.js
@@ -0,0 +1,194 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function verifyHighlights(
+ browser,
+ data,
+ productUrl /* optional, set to override */,
+ expectedHighlightTypes,
+ expectedLang
+) {
+ return SpecialPowers.spawn(
+ browser,
+ [{ data, productUrl, expectedHighlightTypes, expectedLang }],
+ async args => {
+ let shoppingContainer =
+ content.document.querySelector("shopping-container").wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(args.data, content);
+ if (args.productUrl) {
+ shoppingContainer.productUrl = args.productUrl;
+ }
+ await shoppingContainer.updateComplete;
+
+ let reviewHighlights = shoppingContainer.highlightsEl;
+ ok(reviewHighlights, "Got review-highlights");
+ await reviewHighlights.updateComplete;
+
+ let highlightsList = reviewHighlights.reviewHighlightsListEl;
+ await highlightsList.updateComplete;
+
+ is(
+ highlightsList.children.length,
+ args.expectedHighlightTypes.length,
+ "review-highlights should have the right number of highlight-items"
+ );
+
+ // Verify number of reviews for each available highlight
+ for (let key of args.expectedHighlightTypes) {
+ let highlightEl = highlightsList.querySelector(
+ `#${content.CSS.escape(key)}`
+ );
+
+ ok(highlightEl, "highlight-item for " + key + " exists");
+ is(
+ highlightEl.lang,
+ args.expectedLang,
+ `highlight-item should have lang set to ${args.expectedLang}`
+ );
+
+ let actualNumberOfReviews = highlightEl.shadowRoot.querySelector(
+ ".highlight-details-list"
+ ).children.length;
+ let expectedNumberOfReviews = Object.values(
+ args.data.highlights[key]
+ ).flat().length;
+
+ is(
+ actualNumberOfReviews,
+ expectedNumberOfReviews,
+ "There should be equal number of reviews displayed for " + key
+ );
+ }
+ }
+ );
+}
+
+/**
+ * Tests that the review highlights custom components are visible on the page
+ * if there is valid data.
+ */
+add_task(async function test_review_highlights() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ let data = MOCK_ANALYZED_PRODUCT_RESPONSE;
+ let expectedHighlightTypes = [
+ "price",
+ "quality",
+ "competitiveness",
+ "packaging/appearance",
+ ];
+
+ info("Testing with default en highlights");
+ await verifyHighlights(
+ browser,
+ data,
+ undefined,
+ expectedHighlightTypes,
+ "en"
+ );
+
+ info("Testing with www.amazon.fr");
+ await verifyHighlights(
+ browser,
+ data,
+ "https://www.amazon.fr",
+ expectedHighlightTypes,
+ "fr"
+ );
+
+ info("Testing with www.amazon.de");
+ await verifyHighlights(
+ browser,
+ data,
+ "https://www.amazon.de",
+ expectedHighlightTypes,
+ "de"
+ );
+ }
+ );
+});
+
+/**
+ * Tests that entire highlights components is still hidden if we receive falsy data.
+ */
+add_task(async function test_review_highlights_no_highlights() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [MOCK_ANALYZED_PRODUCT_RESPONSE],
+ async mockData => {
+ mockData.highlights = null;
+
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+
+ let reviewHighlights = shoppingContainer.highlightsEl;
+ ok(reviewHighlights, "Got review-highlights");
+ await reviewHighlights.updateComplete;
+
+ ok(
+ ContentTaskUtils.isHidden(reviewHighlights),
+ "review-highlights should not be visible"
+ );
+
+ let highlightsList = reviewHighlights?.reviewHighlightsListEl;
+ ok(!highlightsList, "review-highlights-list should not be visible");
+ }
+ );
+ }
+ );
+});
+
+/**
+ * Tests that we do not show an invalid highlight type and properly filter data.
+ */
+add_task(async function test_review_highlights_invalid_type() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ const invalidHighlightData = structuredClone(
+ MOCK_ANALYZED_PRODUCT_RESPONSE
+ );
+ invalidHighlightData.highlights = MOCK_INVALID_KEY_OBJ;
+ await SpecialPowers.spawn(
+ browser,
+ [invalidHighlightData],
+ async mockData => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+
+ let reviewHighlights = shoppingContainer.highlightsEl;
+ ok(reviewHighlights, "Got review-highlights");
+ await reviewHighlights.updateComplete;
+
+ ok(
+ ContentTaskUtils.isHidden(reviewHighlights),
+ "review-highlights should not be visible"
+ );
+ }
+ );
+ }
+ );
+});
diff --git a/browser/components/shopping/tests/browser/browser_settings_telemetry.js b/browser/components/shopping/tests/browser/browser_settings_telemetry.js
new file mode 100644
index 0000000000..803630f73d
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_settings_telemetry.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the settings component is rendered as expected.
+ */
+add_task(async function test_shopping_settings() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["toolkit.telemetry.testing.overridePreRelease", true],
+ ["browser.shopping.experience2023.optedIn", 0],
+ ],
+ });
+
+ let opt_in_status = Services.prefs.getIntPref(
+ "browser.shopping.experience2023.optedIn",
+ undefined
+ );
+ // Values that match how we're defining the metrics
+ let component_opted_out = opt_in_status === 2;
+ let onboarded_status = opt_in_status > 0;
+
+ Assert.equal(
+ component_opted_out,
+ Glean.shoppingSettings.componentOptedOut.testGetValue(),
+ "Component Opted Out metric should correctly reflect the preference value"
+ );
+ Assert.equal(
+ onboarded_status,
+ Glean.shoppingSettings.hasOnboarded.testGetValue(),
+ "Has Onboarded metric should correctly reflect the preference value"
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_shopping_setting_update() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["toolkit.telemetry.testing.overridePreRelease", true],
+ ["browser.shopping.experience2023.optedIn", 2],
+ ],
+ });
+
+ Assert.equal(
+ true,
+ Glean.shoppingSettings.componentOptedOut.testGetValue(),
+ "Component Opted Out metric should return True as we've set the value of the preference"
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_shopping_settings_ads_enabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.shopping.experience2023.optedIn", 1]],
+ });
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [MOCK_ANALYZED_PRODUCT_RESPONSE],
+ async mockData => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ shoppingContainer.adsEnabled = true;
+ await shoppingContainer.updateComplete;
+
+ let shoppingSettings = shoppingContainer.settingsEl;
+ ok(shoppingSettings, "Got the shopping-settings element");
+
+ let optOutButton = shoppingSettings.optOutButtonEl;
+ ok(optOutButton, "There should be an opt-out button");
+
+ optOutButton.click();
+ }
+ );
+ }
+ );
+
+ await Services.fog.testFlushAllChildren();
+ var optOutClickedEvents =
+ Glean.shopping.surfaceOptOutButtonClicked.testGetValue();
+
+ Assert.equal(optOutClickedEvents.length, 1);
+ Assert.equal(optOutClickedEvents[0].category, "shopping");
+ Assert.equal(optOutClickedEvents[0].name, "surface_opt_out_button_clicked");
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/shopping/tests/browser/browser_shopping_card.js b/browser/components/shopping/tests/browser/browser_shopping_card.js
new file mode 100644
index 0000000000..ebe35f1cc0
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_shopping_card.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the chevron button's accessible name and state.
+ */
+add_task(async function test_chevron_button_markup() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [MOCK_UNANALYZED_PRODUCT_RESPONSE],
+ async mockData => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+
+ let shoppingSettings = content.document
+ .querySelector("shopping-container")
+ .shadowRoot.querySelector("shopping-settings");
+ let shoppingCard =
+ shoppingSettings.shadowRoot.querySelector("shopping-card");
+ let detailsEl = shoppingCard.shadowRoot.querySelector("details");
+
+ // Need to wait for different async events to complete on the lit component:
+ await ContentTaskUtils.waitForCondition(() =>
+ detailsEl.querySelector(".chevron-icon")
+ );
+
+ let chevronButton = detailsEl.querySelector(".chevron-icon");
+
+ is(
+ chevronButton.getAttribute("aria-labelledby"),
+ "header",
+ "The chevron button is has an accessible name"
+ );
+ }
+ );
+ }
+ );
+});
diff --git a/browser/components/shopping/tests/browser/browser_shopping_container.js b/browser/components/shopping/tests/browser/browser_shopping_container.js
new file mode 100644
index 0000000000..533f40f33e
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_shopping_container.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_close_button() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ // Call SpecialPowers.spawn to make RPMSetPref available on the content window.
+ await SpecialPowers.spawn(
+ browser,
+ [MOCK_ANALYZED_PRODUCT_RESPONSE],
+ async () => {
+ let { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+ );
+
+ let xrayWindow = ChromeUtils.waiveXrays(content);
+ let setPrefSpy = sinon.spy(xrayWindow, "RPMSetPref");
+
+ let closeButton = content.document
+ .querySelector("shopping-container")
+ .shadowRoot.querySelector("#close-button");
+ closeButton.click();
+
+ ok(
+ setPrefSpy.calledOnceWith(
+ "browser.shopping.experience2023.active",
+ false
+ )
+ );
+ setPrefSpy.restore();
+ }
+ );
+ }
+ );
+});
diff --git a/browser/components/shopping/tests/browser/browser_shopping_message_triggers.js b/browser/components/shopping/tests/browser/browser_shopping_message_triggers.js
new file mode 100644
index 0000000000..47e4e2a1a7
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_shopping_message_triggers.js
@@ -0,0 +1,315 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ShoppingUtils: "resource:///modules/ShoppingUtils.sys.mjs",
+});
+
+const { ASRouter } = ChromeUtils.importESModule(
+ "resource:///modules/asrouter/ASRouter.sys.mjs"
+);
+
+const { FeatureCalloutMessages } = ChromeUtils.importESModule(
+ "resource:///modules/asrouter/FeatureCalloutMessages.sys.mjs"
+);
+
+const OPTED_IN_PREF = "browser.shopping.experience2023.optedIn";
+const ACTIVE_PREF = "browser.shopping.experience2023.active";
+const CFR_ENABLED_PREF =
+ "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features";
+
+const CONTENT_PAGE = "https://example.com";
+const PRODUCT_PAGE = "https://example.com/product/B09TJGHL5F";
+
+add_setup(async function setup() {
+ // disable auto-activation to prevent interference with the tests
+ ShoppingUtils.handledAutoActivate = true;
+ // clean up all the prefs/states modified by this test
+ registerCleanupFunction(() => {
+ ShoppingUtils.handledAutoActivate = false;
+ });
+});
+
+/** Test that the correct callouts show for opted-in users */
+add_task(async function test_fakespot_callouts_opted_in_flow() {
+ // Re-enable feature callouts for this test. This has to be done in each task
+ // because they're disabled in browser.ini.
+ await SpecialPowers.pushPrefEnv({ set: [[CFR_ENABLED_PREF, true]] });
+ let sandbox = sinon.createSandbox();
+ let routeCFRMessageStub = sandbox
+ .stub(ASRouter, "routeCFRMessage")
+ .withArgs(
+ sinon.match.any,
+ sinon.match.any,
+ sinon.match({ id: "shoppingProductPageWithSidebarClosed" })
+ );
+ await ASRouter._updateMessageProviders();
+ await ASRouter.loadMessagesFromAllProviders(
+ ASRouter.state.providers.filter(p => p.id === "onboarding")
+ );
+
+ // Reset opt-in but make the sidebar active so it appears on PDP.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [ACTIVE_PREF, true],
+ [OPTED_IN_PREF, 0],
+ ],
+ });
+
+ // Visit a product page and wait for the sidebar to open.
+ let pdpTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ PRODUCT_PAGE
+ );
+ let pdpBrowser = pdpTab.linkedBrowser;
+ let pdpBrowserPanel = gBrowser.getPanel(pdpBrowser);
+ let isSidebarVisible = () => {
+ let sidebar = pdpBrowserPanel.querySelector("shopping-sidebar");
+ return sidebar && BrowserTestUtils.isVisible(sidebar);
+ };
+ await BrowserTestUtils.waitForMutationCondition(
+ pdpBrowserPanel,
+ { childList: true, attributeFilter: ["hidden"] },
+ isSidebarVisible
+ );
+ ok(isSidebarVisible(), "Shopping sidebar should be open on a product page");
+
+ // Visiting the PDP should not cause shoppingProductPageWithSidebarClosed to
+ // fire in this case, because the sidebar is active.
+ ok(
+ routeCFRMessageStub.neverCalledWithMatch(
+ sinon.match.any,
+ sinon.match.any,
+ sinon.match({ id: "shoppingProductPageWithSidebarClosed" })
+ ),
+ "shoppingProductPageWithSidebarClosed should not fire when sidebar is active"
+ );
+
+ // Now opt in...
+ let prefChanged = TestUtils.waitForPrefChange(
+ OPTED_IN_PREF,
+ value => value === 1
+ );
+ await SpecialPowers.pushPrefEnv({ set: [[OPTED_IN_PREF, 1]] });
+ await prefChanged;
+
+ // Close the sidebar by deactivating the global toggle, and wait for the
+ // shoppingProductPageWithSidebarClosed trigger to fire.
+ let shoppingProductPageWithSidebarClosedMsg = new Promise(resolve => {
+ routeCFRMessageStub.callsFake((message, browser, trigger) => {
+ if (
+ trigger.id === "shoppingProductPageWithSidebarClosed" &&
+ trigger.context.isSidebarClosing
+ ) {
+ resolve(message?.id);
+ }
+ });
+ });
+ await SpecialPowers.pushPrefEnv({ set: [[ACTIVE_PREF, false]] });
+ // Assert that the message is the one we expect.
+ is(
+ await shoppingProductPageWithSidebarClosedMsg,
+ "FAKESPOT_CALLOUT_CLOSED_OPTED_IN_DEFAULT",
+ "Should route the expected message: FAKESPOT_CALLOUT_CLOSED_OPTED_IN_DEFAULT"
+ );
+ BrowserTestUtils.removeTab(pdpTab);
+
+ // Now, having seen the on-closed callout, we should expect to see the on-PDP
+ // callout on the next PDP visit, provided it's been at least 24 hours.
+ //
+ // Of course we can't really do that in an automated test, so we'll override
+ // the message impression date to simulate that.
+ //
+ // But first, try opening a PDP so we can test that it _doesn't_ fire if less
+ // than 24hrs has passed.
+
+ // Visit a product page and wait for routeCFRMessage to fire, expecting the
+ // message to be null due to targeting.
+ shoppingProductPageWithSidebarClosedMsg = new Promise(resolve => {
+ routeCFRMessageStub.callsFake((message, browser, trigger) => {
+ if (
+ trigger.id === "shoppingProductPageWithSidebarClosed" &&
+ !trigger.context.isSidebarClosing
+ ) {
+ resolve(message?.id);
+ }
+ });
+ });
+ pdpTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PRODUCT_PAGE);
+ // Assert that the on-PDP message is not matched, due to targeting.
+ isnot(
+ await shoppingProductPageWithSidebarClosedMsg,
+ "FAKESPOT_CALLOUT_PDP_OPTED_IN_DEFAULT",
+ "Should not route the on-PDP message because the on-close message was seen recently"
+ );
+ BrowserTestUtils.removeTab(pdpTab);
+
+ // Now override the state so it looks like we closed the sidebar 25 hours ago.
+ let lastClosedDate = Date.now() - 25 * 60 * 60 * 1000; // 25 hours ago
+ await ASRouter.setState(state => {
+ const messageImpressions = { ...state.messageImpressions };
+ messageImpressions.FAKESPOT_CALLOUT_CLOSED_OPTED_IN_DEFAULT = [
+ lastClosedDate,
+ ];
+ ASRouter._storage.set("messageImpressions", messageImpressions);
+ return { messageImpressions };
+ });
+
+ // And open a new PDP, expecting the on-PDP message to be routed.
+ shoppingProductPageWithSidebarClosedMsg = new Promise(resolve => {
+ routeCFRMessageStub.callsFake((message, browser, trigger) => {
+ if (
+ trigger.id === "shoppingProductPageWithSidebarClosed" &&
+ !trigger.context.isSidebarClosing
+ ) {
+ resolve(message?.id);
+ }
+ });
+ });
+ pdpTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PRODUCT_PAGE);
+ // Assert that the on-PDP message is now matched, due to targeting.
+ is(
+ await shoppingProductPageWithSidebarClosedMsg,
+ "FAKESPOT_CALLOUT_PDP_OPTED_IN_DEFAULT",
+ "Should route the on-PDP message"
+ );
+ BrowserTestUtils.removeTab(pdpTab);
+
+ // Clean up.
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.popPrefEnv();
+ await ASRouter.setState(state => {
+ const messageImpressions = { ...state.messageImpressions };
+ delete messageImpressions.FAKESPOT_CALLOUT_CLOSED_OPTED_IN_DEFAULT;
+ ASRouter._storage.set("messageImpressions", messageImpressions);
+ return { messageImpressions };
+ });
+ sandbox.restore();
+ await ASRouter._updateMessageProviders();
+ await ASRouter.loadMessagesFromAllProviders(
+ ASRouter.state.providers.filter(p => p.id === "onboarding")
+ );
+});
+
+/** Test that the correct callouts show for not-opted-in users */
+add_task(async function test_fakespot_callouts_not_opted_in_flow() {
+ await SpecialPowers.pushPrefEnv({ set: [[CFR_ENABLED_PREF, true]] });
+ let sandbox = sinon.createSandbox();
+ let routeCFRMessageStub = sandbox
+ .stub(ASRouter, "routeCFRMessage")
+ .withArgs(
+ sinon.match.any,
+ sinon.match.any,
+ sinon.match({ id: "shoppingProductPageWithSidebarClosed" })
+ );
+ await ASRouter._updateMessageProviders();
+ await ASRouter.loadMessagesFromAllProviders(
+ ASRouter.state.providers.filter(p => p.id === "onboarding")
+ );
+
+ // Reset opt-in but make the sidebar active so it appears on PDP.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [ACTIVE_PREF, true],
+ [OPTED_IN_PREF, 0],
+ ],
+ });
+
+ // Visit a product page and wait for the sidebar to open.
+ let pdpTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ PRODUCT_PAGE
+ );
+ let pdpBrowser = pdpTab.linkedBrowser;
+ let pdpBrowserPanel = gBrowser.getPanel(pdpBrowser);
+ let isSidebarVisible = () => {
+ let sidebar = pdpBrowserPanel.querySelector("shopping-sidebar");
+ return sidebar && BrowserTestUtils.isVisible(sidebar);
+ };
+ await BrowserTestUtils.waitForMutationCondition(
+ pdpBrowserPanel,
+ { childList: true, attributeFilter: ["hidden"] },
+ isSidebarVisible
+ );
+ ok(isSidebarVisible(), "Shopping sidebar should be open on a product page");
+
+ // Close the sidebar by deactivating the global toggle, and wait for the
+ // shoppingProductPageWithSidebarClosed trigger to fire.
+ let shoppingProductPageWithSidebarClosedMsg = new Promise(resolve => {
+ routeCFRMessageStub.callsFake((message, browser, trigger) => {
+ if (
+ trigger.id === "shoppingProductPageWithSidebarClosed" &&
+ trigger.context.isSidebarClosing
+ ) {
+ resolve(message?.id);
+ }
+ });
+ });
+ await SpecialPowers.pushPrefEnv({ set: [[ACTIVE_PREF, false]] });
+ // Assert that the message is the one we expect.
+ is(
+ await shoppingProductPageWithSidebarClosedMsg,
+ "FAKESPOT_CALLOUT_CLOSED_NOT_OPTED_IN_DEFAULT",
+ "Should route the expected message: FAKESPOT_CALLOUT_CLOSED_NOT_OPTED_IN_DEFAULT"
+ );
+ BrowserTestUtils.removeTab(pdpTab);
+
+ // Unlike the opted-in flow, at this point we should not expect to see any
+ // more callouts, because the flow ends after the on-closed callout. So we can
+ // test that FAKESPOT_CALLOUT_CLOSED_NOT_OPTED_IN_DEFAULT's targeting excludes us
+ // even if it's been 25 hours since the sidebar was closed.
+
+ // As with the opted-in flow, override the state so it looks like we closed
+ // the sidebar 25 hours ago.
+ let lastClosedDate = Date.now() - 25 * 60 * 60 * 1000; // 25 hours ago
+ await ASRouter.setState(state => {
+ const messageImpressions = { ...state.messageImpressions };
+ messageImpressions.FAKESPOT_CALLOUT_CLOSED_NOT_OPTED_IN_DEFAULT = [
+ lastClosedDate,
+ ];
+ ASRouter._storage.set("messageImpressions", messageImpressions);
+ return { messageImpressions };
+ });
+
+ // Visit a product page and wait for routeCFRMessage to fire, expecting the
+ // message to be null due to targeting.
+ shoppingProductPageWithSidebarClosedMsg = new Promise(resolve => {
+ routeCFRMessageStub.callsFake((message, browser, trigger) => {
+ if (
+ trigger.id === "shoppingProductPageWithSidebarClosed" &&
+ !trigger.context.isSidebarClosing
+ ) {
+ resolve(message?.id);
+ }
+ });
+ });
+ pdpTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PRODUCT_PAGE);
+ // Assert that the on-PDP message is not matched, due to targeting.
+ isnot(
+ await shoppingProductPageWithSidebarClosedMsg,
+ "FAKESPOT_CALLOUT_PDP_OPTED_IN_DEFAULT",
+ "Should not route the on-PDP message because the user is not opted in"
+ );
+ BrowserTestUtils.removeTab(pdpTab);
+
+ // Clean up. We don't need to verify that the frequency caps work, since
+ // that's a generic ASRouter feature.
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.popPrefEnv();
+ await ASRouter.setState(state => {
+ const messageImpressions = { ...state.messageImpressions };
+ delete messageImpressions.FAKESPOT_CALLOUT_CLOSED_NOT_OPTED_IN_DEFAULT;
+ ASRouter._storage.set("messageImpressions", messageImpressions);
+ return { messageImpressions };
+ });
+ sandbox.restore();
+ await ASRouter._updateMessageProviders();
+ await ASRouter.loadMessagesFromAllProviders(
+ ASRouter.state.providers.filter(p => p.id === "onboarding")
+ );
+});
diff --git a/browser/components/shopping/tests/browser/browser_shopping_onboarding.js b/browser/components/shopping/tests/browser/browser_shopping_onboarding.js
new file mode 100644
index 0000000000..725ce6d8c2
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_shopping_onboarding.js
@@ -0,0 +1,661 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ShoppingUtils: "resource:///modules/ShoppingUtils.sys.mjs",
+});
+
+const { SpecialMessageActions } = ChromeUtils.importESModule(
+ "resource://messaging-system/lib/SpecialMessageActions.sys.mjs"
+);
+
+/**
+ * Toggle prefs involved in automatically activating the sidebar on PDPs if the
+ * user has not opted in. Onboarding should only try to auto-activate the
+ * sidebar for non-opted-in users once per session at most, no more than once
+ * per day, and no more than two times total.
+ *
+ * @param {object} states An object containing pref states to set. Leave a
+ * property undefined to ignore it.
+ * @param {boolean} [states.active] Global sidebar toggle
+ * @param {number} [states.optedIn] 2: opted out, 1: opted in, 0: not opted in
+ * @param {number} [states.lastAutoActivate] Last auto activate date in seconds
+ * @param {number} [states.autoActivateCount] Number of auto-activations (max 2)
+ * @param {boolean} [states.handledAutoActivate] True if the sidebar handled its
+ * auto-activation logic this session, preventing further auto-activations
+ */
+function setOnboardingPrefs(states = {}) {
+ if (Object.hasOwn(states, "handledAutoActivate")) {
+ ShoppingUtils.handledAutoActivate = !!states.handledAutoActivate;
+ }
+
+ if (Object.hasOwn(states, "lastAutoActivate")) {
+ Services.prefs.setIntPref(
+ "browser.shopping.experience2023.lastAutoActivate",
+ states.lastAutoActivate
+ );
+ }
+
+ if (Object.hasOwn(states, "autoActivateCount")) {
+ Services.prefs.setIntPref(
+ "browser.shopping.experience2023.autoActivateCount",
+ states.autoActivateCount
+ );
+ }
+
+ if (Object.hasOwn(states, "optedIn")) {
+ Services.prefs.setIntPref(
+ "browser.shopping.experience2023.optedIn",
+ states.optedIn
+ );
+ }
+
+ if (Object.hasOwn(states, "active")) {
+ Services.prefs.setBoolPref(
+ "browser.shopping.experience2023.active",
+ states.active
+ );
+ }
+
+ if (Object.hasOwn(states, "telemetryEnabled")) {
+ Services.prefs.setBoolPref(
+ "browser.newtabpage.activity-stream.telemetry",
+ states.telemetryEnabled
+ );
+ }
+}
+
+add_setup(async function setup() {
+ // Block on testFlushAllChildren to ensure Glean is initialized before
+ // running tests.
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+ // Set all the prefs/states modified by this test to default values.
+ registerCleanupFunction(() =>
+ setOnboardingPrefs({
+ active: true,
+ optedIn: 1,
+ lastAutoActivate: 0,
+ autoActivateCount: 0,
+ handledAutoActivate: false,
+ telementryEnabled: false,
+ })
+ );
+});
+
+/**
+ * Test to check onboarding message container is rendered
+ * when user is not opted-in
+ */
+add_task(async function test_showOnboarding_notOptedIn() {
+ // OptedIn pref Value is 0 when a user hasn't opted-in
+ setOnboardingPrefs({ active: false, optedIn: 0, telemetryEnabled: true });
+
+ Services.fog.testResetFOG();
+ await Services.fog.testFlushAllChildren();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ // Get the actor to update the product URL, since no content will render without one
+ let actor =
+ gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getExistingActor(
+ "ShoppingSidebar"
+ );
+ actor.updateProductURL("https://example.com/product/B09TJGHL5F");
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ let shoppingContainer = await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelector("shopping-container"),
+ "shopping-container"
+ );
+
+ let containerElem =
+ shoppingContainer.shadowRoot.getElementById("shopping-container");
+ let messageSlot = containerElem.getElementsByTagName("slot");
+
+ // Check multi-stage-message-slot used to show opt-In message is
+ // rendered inside shopping container when user optedIn pref value is 0
+ ok(messageSlot.length, `message slot element exists`);
+ is(
+ messageSlot[0].name,
+ "multi-stage-message-slot",
+ "multi-stage-message-slot showing opt-in message rendered"
+ );
+
+ ok(
+ !content.document.getElementById("multi-stage-message-root").hidden,
+ "message is shown"
+ );
+ });
+ }
+ );
+
+ if (!AppConstants.platform != "linux") {
+ await Services.fog.testFlushAllChildren();
+ const events = Glean.shopping.surfaceOnboardingDisplayed.testGetValue();
+
+ if (events) {
+ Assert.greater(events.length, 0);
+ Assert.equal(events[0].category, "shopping");
+ Assert.equal(events[0].name, "surface_onboarding_displayed");
+ } else {
+ info("Failed to get Glean value due to unknown bug. See bug 1862389.");
+ }
+ }
+});
+
+/**
+ * Test to check onboarding message is not shown for opted-in users
+ */
+add_task(async function test_hideOnboarding_optedIn() {
+ // OptedIn pref value is 1 for opted-in users
+ setOnboardingPrefs({ active: false, optedIn: 1 });
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ // Get the actor to update the product URL, since no content will render without one
+ let actor =
+ gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getExistingActor(
+ "ShoppingSidebar"
+ );
+ actor.updateProductURL("https://example.com/product/B09TJGHL5F");
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelector("shopping-container"),
+ "shopping-container"
+ );
+
+ ok(
+ content.document.getElementById("multi-stage-message-root").hidden,
+ "message is hidden"
+ );
+ });
+ }
+ );
+});
+
+/**
+ * Test to check onboarding message does not show when selecting "not now"
+ *
+ * Also confirms a Glean event was triggered.
+ */
+add_task(async function test_hideOnboarding_onClose() {
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+ // OptedIn pref value is 0 when a user has not opted-in
+ setOnboardingPrefs({ active: false, optedIn: 0, telemetryEnabled: true });
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ // Get the actor to update the product URL, since no content will render without one
+ let actor =
+ gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getExistingActor(
+ "ShoppingSidebar"
+ );
+ actor.updateProductURL("https://example.com/product/B09TJGHL5F");
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ let shoppingContainer = await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelector("shopping-container"),
+ "shopping-container"
+ );
+ // "Not now" button
+ let notNowButton = await ContentTaskUtils.waitForCondition(() =>
+ shoppingContainer.querySelector(".additional-cta")
+ );
+
+ notNowButton.click();
+
+ // Does not render shopping container onboarding message
+ ok(
+ !shoppingContainer.length,
+ "Shopping container element does not exist"
+ );
+ });
+ }
+ );
+
+ await Services.fog.testFlushAllChildren();
+ let events = Glean.shopping.surfaceNotNowClicked.testGetValue();
+
+ await BrowserTestUtils.waitForCondition(() => {
+ let _events = Glean.shopping.surfaceNotNowClicked.testGetValue();
+ return _events?.length > 0;
+ });
+
+ Assert.greater(events.length, 0);
+ Assert.equal(events[0].category, "shopping");
+ Assert.equal(events[0].name, "surface_not_now_clicked");
+});
+
+/**
+ * Test to check behavior when selecting 'Yes, try it to opt in to the
+ * shopping experience.
+ *
+ * Also tests if a Glean event was correctly recorded.
+ */
+add_task(async function test_onOptIn() {
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+ setOnboardingPrefs({ active: false, optedIn: 0, telemetryEnabled: true });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(browser, [], async () => {
+ await ContentTaskUtils.waitForMutationCondition(
+ content.document,
+ { childList: true, subtree: true },
+ () => !!content.document.querySelector("shopping-container .primary")
+ );
+
+ // "Yes, try it" button
+ let primary = content.document.querySelector(
+ "shopping-container .primary"
+ );
+ primary.click();
+ });
+ }
+ );
+
+ await Services.fog.testFlushAllChildren();
+ let events = Glean.shopping.surfaceOptInClicked.testGetValue();
+
+ await BrowserTestUtils.waitForCondition(() => {
+ let _events = Glean.shopping.surfaceOptInClicked.testGetValue();
+ return _events?.length > 0;
+ });
+
+ Assert.greater(events.length, 0);
+ Assert.equal(events[0].category, "shopping");
+ Assert.equal(events[0].name, "surface_opt_in_clicked");
+});
+
+/**
+ * Helper function to click the links in the Link Paragraph.
+ */
+async function linkParagraphClickLinks() {
+ const sandbox = sinon.createSandbox();
+
+ let handleActionStub = sandbox
+ .stub(SpecialMessageActions, "handleAction")
+ .withArgs(sandbox.match({ type: "OPEN_URL" }));
+
+ let handleActionStubCalled = new Promise(resolve =>
+ handleActionStub.callsFake(resolve)
+ );
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(browser, [], async () => {
+ await ContentTaskUtils.waitForMutationCondition(
+ content.document,
+ { childList: true, subtree: true },
+ // Can safely assume that if one of the link exists, they both do.
+ () =>
+ !!content.document.querySelector(
+ ".legal-paragraph a[value='terms_of_use']"
+ )
+ );
+
+ let termsOfUse = content.document.querySelector(
+ "shopping-container .legal-paragraph a[value='terms_of_use']"
+ );
+ termsOfUse.click();
+ });
+ }
+ );
+
+ await handleActionStubCalled;
+
+ handleActionStub.resetHistory();
+
+ handleActionStubCalled = new Promise(resolve =>
+ handleActionStub.callsFake(resolve)
+ );
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(browser, [], async () => {
+ await ContentTaskUtils.waitForMutationCondition(
+ content.document,
+ { childList: true, subtree: true },
+ // Can safely assume that if one of the link exists, they both do.
+ () =>
+ !!content.document.querySelector(
+ ".legal-paragraph a[value='terms_of_use']"
+ )
+ );
+ let privacyPolicy = content.document.querySelector(
+ "shopping-container .legal-paragraph a[value='privacy_policy']"
+ );
+ privacyPolicy.click();
+ });
+ }
+ );
+ await handleActionStubCalled;
+
+ handleActionStub.resetHistory();
+
+ handleActionStubCalled = new Promise(resolve =>
+ handleActionStub.callsFake(resolve)
+ );
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(browser, [], async () => {
+ await ContentTaskUtils.waitForMutationCondition(
+ content.document,
+ { childList: true, subtree: true },
+ () => content.document.querySelector(".link-paragraph a")
+ );
+ let learnMore = content.document.querySelector(
+ "shopping-container .link-paragraph a[value='learn_more']"
+ );
+ // Learn More link button.
+ learnMore.click();
+ });
+ }
+ );
+ await handleActionStubCalled;
+
+ sandbox.restore();
+}
+
+/**
+ * Test to check behavior when selecting links in the link-paragraph
+ * to opt in to the
+ * shopping experience.
+ *
+ * Also tests if a Glean event was correctly recorded.
+ */
+add_task(async function test_linkParagraph() {
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+ setOnboardingPrefs({ active: false, optedIn: 0, telemetryEnabled: true });
+
+ await linkParagraphClickLinks();
+
+ await Services.fog.testFlushAllChildren();
+ let privacyEvents =
+ Glean.shopping.surfaceShowPrivacyPolicyClicked.testGetValue();
+
+ Assert.greater(privacyEvents.length, 0);
+ Assert.equal(privacyEvents[0].category, "shopping");
+ Assert.equal(privacyEvents[0].name, "surface_show_privacy_policy_clicked");
+
+ let tosEvents = Glean.shopping.surfaceShowTermsClicked.testGetValue();
+
+ Assert.greater(tosEvents.length, 0);
+ Assert.equal(tosEvents[0].category, "shopping");
+ Assert.equal(tosEvents[0].name, "surface_show_terms_clicked");
+
+ let learnMoreEvents = Glean.shopping.surfaceLearnMoreClicked.testGetValue();
+
+ Assert.greater(learnMoreEvents.length, 0);
+ Assert.equal(learnMoreEvents[0].category, "shopping");
+ Assert.equal(learnMoreEvents[0].name, "surface_learn_more_clicked");
+});
+
+add_task(async function test_onboarding_auto_activate_opt_in() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
+ true,
+ ],
+ ],
+ });
+ // Opt out of the feature
+ setOnboardingPrefs({
+ active: false,
+ optedIn: 0,
+ lastAutoActivate: 0,
+ autoActivateCount: 0,
+ handledAutoActivate: false,
+ });
+ ShoppingUtils.handleAutoActivateOnProduct();
+
+ // User is not opted-in, and auto-activate has not happened yet. So it should
+ // be enabled now.
+ ok(
+ Services.prefs.getBoolPref("browser.shopping.experience2023.active"),
+ "Global toggle should be activated to open the sidebar on PDPs"
+ );
+
+ // Now opt in, deactivate the global toggle, and reset the targeting prefs.
+ // The sidebar should no longer open on PDPs, since the user is opted in and
+ // the global toggle is off.
+
+ setOnboardingPrefs({
+ active: false,
+ optedIn: 1,
+ lastAutoActivate: 0,
+ autoActivateCount: 1,
+ handledAutoActivate: false,
+ });
+ ShoppingUtils.handleAutoActivateOnProduct();
+
+ ok(
+ !Services.prefs.getBoolPref("browser.shopping.experience2023.active"),
+ "Global toggle should not activate again since user is opted in"
+ );
+});
+
+add_task(async function test_onboarding_auto_activate_not_now() {
+ // Opt of the feature so it auto-activates once.
+ setOnboardingPrefs({
+ active: false,
+ optedIn: 0,
+ lastAutoActivate: 0,
+ autoActivateCount: 0,
+ handledAutoActivate: false,
+ });
+ ShoppingUtils.handleAutoActivateOnProduct();
+
+ ok(
+ Services.prefs.getBoolPref("browser.shopping.experience2023.active"),
+ "Global toggle should be activated to open the sidebar on PDPs"
+ );
+
+ // After auto-activating once, we should not auto-activate again in this
+ // session. So when we click "Not now", it should deactivate the global
+ // toggle, closing all sidebars, and sidebars should not open again on PDPs.
+ // Test that handledAutoActivate was set automatically by the previous
+ // auto-activate, and that it prevents the toggle from activating again.
+ setOnboardingPrefs({ active: false });
+ ShoppingUtils.handleAutoActivateOnProduct();
+
+ ok(
+ !Services.prefs.getBoolPref("browser.shopping.experience2023.active"),
+ "Global toggle should not activate again this session"
+ );
+
+ // There are 3 conditions for auto-activating the sidebar before opt-in:
+ // 1. The sidebar has not already been automatically set to `active` twice.
+ // 2. It's been at least 24 hours since the user last saw the sidebar because
+ // of this auto-activation behavior.
+ // 3. This method has not already been called (handledAutoActivate is false)
+ // Let's test each of these conditions, in isolation.
+
+ // Reset the auto-activate count to 0, and set the last auto-activate to never
+ // opened. Leave the handledAutoActivate flag set to true, so we can
+ // test that the sidebar auto-activate is still blocked if we already
+ // auto-activated previously this session.
+ setOnboardingPrefs({
+ active: false,
+ optedIn: 0,
+ lastAutoActivate: 0,
+ autoActivateCount: 0,
+ handledAutoActivate: true,
+ });
+ ShoppingUtils.handleAutoActivateOnProduct();
+
+ ok(
+ !Services.prefs.getBoolPref("browser.shopping.experience2023.active"),
+ "Shopping sidebar should not auto-activate if auto-activated previously this session"
+ );
+
+ // Now test that sidebar auto-activate is blocked if the last auto-activate
+ // was less than 24 hours ago.
+ setOnboardingPrefs({
+ active: false,
+ optedIn: 0,
+ lastAutoActivate: Date.now() / 1000,
+ autoActivateCount: 1,
+ handledAutoActivate: false,
+ });
+ ShoppingUtils.handleAutoActivateOnProduct();
+
+ ok(
+ !Services.prefs.getBoolPref("browser.shopping.experience2023.active"),
+ "Shopping sidebar should not auto-activate if last auto-activation was less than 24 hours ago"
+ );
+
+ // Test that auto-activate is blocked if the sidebar has been auto-activated
+ // twice already.
+ setOnboardingPrefs({
+ active: false,
+ optedIn: 0,
+ lastAutoActivate: 0,
+ autoActivateCount: 2,
+ handledAutoActivate: false,
+ });
+ ShoppingUtils.handleAutoActivateOnProduct();
+
+ ok(
+ !Services.prefs.getBoolPref("browser.shopping.experience2023.active"),
+ "Shopping sidebar should not auto-activate if it has already been auto-activated twice"
+ );
+
+ // Now test that auto-activate is unblocked if all 3 conditions are met.
+ setOnboardingPrefs({
+ active: false,
+ optedIn: 0,
+ lastAutoActivate: Date.now() / 1000 - 2 * 24 * 60 * 60, // 2 days ago
+ autoActivateCount: 1,
+ handledAutoActivate: false,
+ });
+ ShoppingUtils.handleAutoActivateOnProduct();
+
+ ok(
+ Services.prefs.getBoolPref("browser.shopping.experience2023.active"),
+ "Shopping sidebar should auto-activate a second time if all conditions are met"
+ );
+});
+
+/**
+ * Test to check onboarding message is not shown for user
+ * after a user opt-out and opt back in after seeing survey
+ */
+
+add_task(async function test_hideOnboarding_OptIn_AfterSurveySeen() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.optedIn", 0],
+ ["browser.shopping.experience2023.survey.enabled", true],
+ ["browser.shopping.experience2023.survey.hasSeen", true],
+ ["browser.shopping.experience2023.survey.pdpVisits", 5],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ let actor =
+ gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getExistingActor(
+ "ShoppingSidebar"
+ );
+ actor.updateProductURL("https://example.com/product/B09TJGHL5F");
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ let shoppingContainer = await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelector("shopping-container"),
+ "shopping-container"
+ );
+
+ ok(
+ !content.document.getElementById("multi-stage-message-root").hidden,
+ "opt-in message is shown"
+ );
+
+ const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+ );
+
+ let optedInPrefChanged = TestUtils.waitForPrefChange(
+ "browser.shopping.experience2023.optedIn",
+ value => value === 1
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.shopping.experience2023.optedIn", 1]],
+ });
+ await optedInPrefChanged;
+ await shoppingContainer.wrappedJSObject.updateComplete;
+
+ ok(
+ content.document.getElementById("multi-stage-message-root").hidden,
+ "opt-in message is hidden"
+ );
+ await SpecialPowers.popPrefEnv();
+ });
+ }
+ );
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_deactivate_sidebar_if_user_turns_off_cfr() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
+ false,
+ ],
+ ],
+ });
+ // Opt out of the feature
+ setOnboardingPrefs({
+ active: false,
+ optedIn: 0,
+ lastAutoActivate: 0,
+ autoActivateCount: 0,
+ handledAutoActivate: false,
+ });
+ ShoppingUtils.handleAutoActivateOnProduct();
+
+ ok(
+ !Services.prefs.getBoolPref("browser.shopping.experience2023.active"),
+ "Shopping sidebar should not auto-activate if Recommended features is turned off"
+ );
+});
diff --git a/browser/components/shopping/tests/browser/browser_shopping_settings.js b/browser/components/shopping/tests/browser/browser_shopping_settings.js
new file mode 100644
index 0000000000..2508be05c7
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_shopping_settings.js
@@ -0,0 +1,642 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the fakespot link has the expected url and utm parameters.
+ */
+add_task(async function test_shopping_settings_fakespot_learn_more() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [MOCK_ANALYZED_PRODUCT_RESPONSE],
+ async mockData => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+
+ let href = shoppingContainer.settingsEl.fakespotLearnMoreLinkEl.href;
+ let url = new URL(href);
+ is(url.pathname, "/our-mission");
+ is(url.origin, "https://www.fakespot.com");
+
+ let qs = url.searchParams;
+ is(qs.get("utm_source"), "review-checker");
+ is(qs.get("utm_campaign"), "fakespot-by-mozilla");
+ is(qs.get("utm_medium"), "inproduct");
+ is(qs.get("utm_term"), "core-sidebar");
+ }
+ );
+ }
+ );
+});
+
+/**
+ * Tests that the ads link has the expected utm parameters.
+ */
+add_task(async function test_shopping_settings_ads_learn_more() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.shopping.experience2023.ads.enabled", true]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [MOCK_ANALYZED_PRODUCT_RESPONSE],
+ async mockData => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+
+ let href = shoppingContainer.settingsEl.adsLearnMoreLinkEl.href;
+ let qs = new URL(href).searchParams;
+
+ is(qs.get("utm_campaign"), "learn-more");
+ is(qs.get("utm_medium"), "inproduct");
+ is(qs.get("utm_term"), "core-sidebar");
+ }
+ );
+ }
+ );
+});
+
+/**
+ * Tests that the settings component is rendered as expected when
+ * `browser.shopping.experience2023.ads.enabled` is true.
+ */
+add_task(async function test_shopping_settings_ads_enabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.shopping.experience2023.ads.enabled", true]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [MOCK_ANALYZED_PRODUCT_RESPONSE],
+ async mockData => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ // Note (Bug 1876878): Until we have proper mocks of data passed from ShoppingSidebarChild,
+ // hardcode `adsEnabled` to be passed to settings.mjs so that we can test
+ // toggle for ad visibility.
+ shoppingContainer.adsEnabled = true;
+ await shoppingContainer.updateComplete;
+
+ let shoppingSettings = shoppingContainer.settingsEl;
+ ok(shoppingSettings, "Got the shopping-settings element");
+
+ let adsToggle = shoppingSettings.recommendationsToggleEl;
+ ok(adsToggle, "There should be an ads toggle");
+
+ let optOutButton = shoppingSettings.optOutButtonEl;
+ ok(optOutButton, "There should be an opt-out button");
+ }
+ );
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Tests that the settings component is rendered as expected when
+ * `browser.shopping.experience2023.ads.enabled` is false.
+ */
+add_task(async function test_shopping_settings_ads_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.shopping.experience2023.ads.enabled", false]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ let shoppingSettings = await getSettingsDetails(
+ browser,
+ MOCK_POPULATED_DATA
+ );
+ ok(shoppingSettings.settingsEl, "Got the shopping-settings element");
+
+ let adsToggle = shoppingSettings.recommendationsToggleEl;
+ ok(!adsToggle, "There should be no ads toggle");
+
+ let optOutButton = shoppingSettings.optOutButtonEl;
+ ok(optOutButton, "There should be an opt-out button");
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Tests that the shopping-settings ads toggle and ad render correctly, even with
+ * multiple tabs. If `browser.shopping.experience2023.ads.userEnabled`
+ * is false in one tab, it should be false for all other tabs with the shopping sidebar open.
+ */
+add_task(async function test_settings_toggle_ad_and_multiple_tabs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.ads.enabled", true],
+ ["browser.shopping.experience2023.ads.userEnabled", true],
+ ],
+ });
+
+ // Tab 1 - ad is visible at first and then toggle is selected to set ads.userEnabled to false.
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:shoppingsidebar"
+ );
+ let browser1 = tab1.linkedBrowser;
+
+ let mockArgs = {
+ mockData: MOCK_ANALYZED_PRODUCT_RESPONSE,
+ mockRecommendationData: MOCK_RECOMMENDED_ADS_RESPONSE,
+ };
+ await SpecialPowers.spawn(browser1, [mockArgs], async args => {
+ const { mockData, mockRecommendationData } = args;
+ let shoppingContainer =
+ content.document.querySelector("shopping-container").wrappedJSObject;
+
+ let adVisiblePromise = ContentTaskUtils.waitForCondition(() => {
+ return (
+ shoppingContainer.recommendedAdEl &&
+ ContentTaskUtils.isVisible(shoppingContainer.recommendedAdEl)
+ );
+ }, "Waiting for recommended-ad to be visible");
+
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ shoppingContainer.recommendationData = Cu.cloneInto(
+ mockRecommendationData,
+ content
+ );
+ // Note (Bug 1876878): Until we have proper mocks of data passed from ShoppingSidebarChild,
+ // hardcode `adsEnabled` and `adsEnabledByUser` so that we can test ad visibility.
+ shoppingContainer.adsEnabled = true;
+ shoppingContainer.adsEnabledByUser = true;
+
+ await shoppingContainer.updateComplete;
+ await adVisiblePromise;
+
+ let adEl = shoppingContainer.recommendedAdEl;
+ await adEl.updateComplete;
+ is(
+ adEl.priceEl.textContent,
+ "$" + mockRecommendationData[0].price,
+ "Price is shown correctly"
+ );
+ is(
+ adEl.linkEl.title,
+ mockRecommendationData[0].name,
+ "Title in link is shown correctly"
+ );
+ is(
+ adEl.linkEl.href,
+ mockRecommendationData[0].url,
+ "URL for link is correct"
+ );
+ is(
+ adEl.ratingEl.rating,
+ mockRecommendationData[0].adjusted_rating,
+ "MozFiveStar rating is shown correctly"
+ );
+ is(
+ adEl.letterGradeEl.letter,
+ mockRecommendationData[0].grade,
+ "LetterGrade letter is shown correctly"
+ );
+
+ let shoppingSettings = shoppingContainer.settingsEl;
+ ok(shoppingSettings, "Got the shopping-settings element");
+
+ let adsToggle = shoppingSettings.recommendationsToggleEl;
+ ok(adsToggle, "There should be a toggle");
+ ok(adsToggle.hasAttribute("pressed"), "Toggle should have enabled state");
+
+ ok(
+ SpecialPowers.getBoolPref(
+ "browser.shopping.experience2023.ads.userEnabled"
+ ),
+ "ads userEnabled pref should be true"
+ );
+
+ let adRemovedPromise = ContentTaskUtils.waitForCondition(() => {
+ return !shoppingContainer.recommendedAdEl;
+ }, "Waiting for recommended-ad to be removed");
+
+ adsToggle.click();
+
+ await adRemovedPromise;
+
+ ok(!adsToggle.hasAttribute("pressed"), "Toggle should have disabled state");
+ ok(
+ !SpecialPowers.getBoolPref(
+ "browser.shopping.experience2023.ads.userEnabled"
+ ),
+ "ads userEnabled pref should be false"
+ );
+ });
+
+ // Tab 2 - ads.userEnabled should still be false and ad should not be visible.
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:shoppingsidebar"
+ );
+ let browser2 = tab2.linkedBrowser;
+
+ await SpecialPowers.spawn(browser2, [mockArgs], async args => {
+ const { mockData, mockRecommendationData } = args;
+ let shoppingContainer =
+ content.document.querySelector("shopping-container").wrappedJSObject;
+
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ shoppingContainer.recommendationData = Cu.cloneInto(
+ mockRecommendationData,
+ content
+ );
+ // Note (Bug 1876878): Until we have proper mocks of data passed from ShoppingSidebarChild,
+ // hardcode `adsEnabled` so that we can test ad visibility.
+ shoppingContainer.adsEnabled = true;
+
+ await shoppingContainer.updateComplete;
+
+ ok(
+ !shoppingContainer.recommendedAdEl,
+ "There should be no ads in the new tab"
+ );
+ ok(
+ !SpecialPowers.getBoolPref(
+ "browser.shopping.experience2023.ads.userEnabled"
+ ),
+ "ads userEnabled pref should be false"
+ );
+ });
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Tests that the settings component is rendered as expected when
+ * `browser.shopping.experience2023.autoOpen.enabled` is false.
+ */
+add_task(async function test_shopping_settings_experiment_auto_open_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.shopping.experience2023.autoOpen.enabled", false]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: PRODUCT_TEST_URL,
+ gBrowser,
+ },
+ async browser => {
+ let sidebar = gBrowser
+ .getPanel(browser)
+ .querySelector("shopping-sidebar");
+ await promiseSidebarUpdated(sidebar, PRODUCT_TEST_URL);
+
+ await SpecialPowers.spawn(
+ sidebar.querySelector("browser"),
+ [MOCK_ANALYZED_PRODUCT_RESPONSE],
+ async mockData => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+
+ let shoppingSettings = shoppingContainer.settingsEl;
+ ok(shoppingSettings, "Got the shopping-settings element");
+ ok(
+ !shoppingSettings.wrapperEl.className.includes(
+ "shopping-settings-auto-open-ui-enabled"
+ ),
+ "Settings card should not have a special classname with autoOpen pref disabled"
+ );
+ is(
+ shoppingSettings.shoppingCardEl?.type,
+ "accordion",
+ "shopping-card type should be accordion"
+ );
+
+ /* Verify control treatment UI */
+ ok(
+ !shoppingSettings.autoOpenToggleEl,
+ "There should be no auto-open toggle"
+ );
+ ok(
+ !shoppingSettings.autoOpenToggleDescriptionEl,
+ "There should be no description for the auto-open toggle"
+ );
+ ok(!shoppingSettings.dividerEl, "There should be no divider");
+ ok(
+ !shoppingSettings.sidebarEnabledStateEl,
+ "There should be no message about the sidebar active state"
+ );
+
+ ok(
+ shoppingSettings.optOutButtonEl,
+ "There should be an opt-out button"
+ );
+ }
+ );
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Tests that the settings component is rendered as expected when
+ * `browser.shopping.experience2023.autoOpen.enabled` is true and
+ * `browser.shopping.experience2023.ads.enabled is true`.
+ */
+add_task(
+ async function test_shopping_settings_experiment_auto_open_enabled_with_ads() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.autoOpen.enabled", true],
+ ["browser.shopping.experience2023.autoOpen.userEnabled", true],
+ ["browser.shopping.experience2023.ads.enabled", true],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: PRODUCT_TEST_URL,
+ gBrowser,
+ },
+ async browser => {
+ let sidebar = gBrowser
+ .getPanel(browser)
+ .querySelector("shopping-sidebar");
+ await promiseSidebarUpdated(sidebar, PRODUCT_TEST_URL);
+
+ await SpecialPowers.spawn(
+ sidebar.querySelector("browser"),
+ [MOCK_ANALYZED_PRODUCT_RESPONSE],
+ async mockData => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+
+ await shoppingContainer.updateComplete;
+
+ let shoppingSettings = shoppingContainer.settingsEl;
+ ok(shoppingSettings, "Got the shopping-settings element");
+ ok(
+ shoppingSettings.wrapperEl.className.includes(
+ "shopping-settings-auto-open-ui-enabled"
+ ),
+ "Settings card should have a special classname with autoOpen pref enabled"
+ );
+ is(
+ shoppingSettings.shoppingCardEl?.type,
+ "",
+ "shopping-card type should be default"
+ );
+
+ ok(
+ shoppingSettings.recommendationsToggleEl,
+ "There should be an ads toggle"
+ );
+
+ /* Verify auto-open experiment UI */
+ ok(
+ shoppingSettings.autoOpenToggleEl,
+ "There should be an auto-open toggle"
+ );
+ ok(
+ shoppingSettings.autoOpenToggleDescriptionEl,
+ "There should be a description for the auto-open toggle"
+ );
+ ok(shoppingSettings.dividerEl, "There should be a divider");
+ ok(
+ shoppingSettings.sidebarEnabledStateEl,
+ "There should be a message about the sidebar active state"
+ );
+
+ ok(
+ shoppingSettings.optOutButtonEl,
+ "There should be an opt-out button"
+ );
+ }
+ );
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.popPrefEnv();
+ }
+);
+
+/**
+ * Tests that the settings component is rendered as expected when
+ * `browser.shopping.experience2023.autoOpen.enabled` is true and
+ * `browser.shopping.experience2023.ads.enabled is false`.
+ */
+add_task(
+ async function test_shopping_settings_experiment_auto_open_enabled_no_ads() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.autoOpen.enabled", true],
+ ["browser.shopping.experience2023.autoOpen.userEnabled", true],
+ ["browser.shopping.experience2023.ads.enabled", false],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: PRODUCT_TEST_URL,
+ gBrowser,
+ },
+ async browser => {
+ let sidebar = gBrowser
+ .getPanel(browser)
+ .querySelector("shopping-sidebar");
+ await promiseSidebarUpdated(sidebar, PRODUCT_TEST_URL);
+
+ await SpecialPowers.spawn(
+ sidebar.querySelector("browser"),
+ [MOCK_ANALYZED_PRODUCT_RESPONSE],
+ async mockData => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+
+ await shoppingContainer.updateComplete;
+
+ let shoppingSettings = shoppingContainer.settingsEl;
+ ok(shoppingSettings, "Got the shopping-settings element");
+ ok(
+ shoppingSettings.wrapperEl.className.includes(
+ "shopping-settings-auto-open-ui-enabled"
+ ),
+ "Settings card should have a special classname with autoOpen pref enabled"
+ );
+ is(
+ shoppingSettings.shoppingCardEl?.type,
+ "",
+ "shopping-card type should be default"
+ );
+
+ ok(
+ !shoppingSettings.recommendationsToggleEl,
+ "There should be no ads toggle"
+ );
+
+ /* Verify auto-open experiment UI */
+ ok(
+ shoppingSettings.autoOpenToggleEl,
+ "There should be an auto-open toggle"
+ );
+ ok(
+ shoppingSettings.autoOpenToggleDescriptionEl,
+ "There should be a description for the auto-open toggle"
+ );
+ ok(shoppingSettings.dividerEl, "There should be a divider");
+ ok(
+ shoppingSettings.sidebarEnabledStateEl,
+ "There should be a message about the sidebar active state"
+ );
+
+ ok(
+ shoppingSettings.optOutButtonEl,
+ "There should be an opt-out button"
+ );
+ }
+ );
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.popPrefEnv();
+ }
+);
+
+/**
+ * Tests that auto-open toggle state and autoOpen.userEnabled pref update correctly.
+ */
+add_task(async function test_settings_auto_open_toggle() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.autoOpen.enabled", true],
+ ["browser.shopping.experience2023.autoOpen.userEnabled", true],
+ ],
+ });
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ PRODUCT_TEST_URL
+ );
+ let browser = tab1.linkedBrowser;
+
+ let mockArgs = {
+ mockData: MOCK_ANALYZED_PRODUCT_RESPONSE,
+ };
+
+ let sidebar = gBrowser.getPanel(browser).querySelector("shopping-sidebar");
+ await promiseSidebarUpdated(sidebar, PRODUCT_TEST_URL);
+
+ await SpecialPowers.spawn(
+ sidebar.querySelector("browser"),
+ [mockArgs],
+ async args => {
+ const { mockData } = args;
+ let shoppingContainer =
+ content.document.querySelector("shopping-container").wrappedJSObject;
+
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+
+ let shoppingSettings = shoppingContainer.settingsEl;
+ ok(shoppingSettings, "Got the shopping-settings element");
+
+ let autoOpenToggle = shoppingSettings.autoOpenToggleEl;
+ ok(autoOpenToggle, "There should be an auto-open toggle");
+ ok(
+ autoOpenToggle.hasAttribute("pressed"),
+ "Toggle should have enabled state"
+ );
+
+ let toggleStateChangePromise = ContentTaskUtils.waitForCondition(() => {
+ return !autoOpenToggle.hasAttribute("pressed");
+ }, "Waiting for auto-open toggle state to be disabled");
+
+ autoOpenToggle.click();
+
+ await toggleStateChangePromise;
+
+ ok(
+ !SpecialPowers.getBoolPref(
+ "browser.shopping.experience2023.autoOpen.userEnabled"
+ ),
+ "autoOpen.userEnabled pref should be false"
+ );
+ ok(
+ SpecialPowers.getBoolPref(
+ "browser.shopping.experience2023.autoOpen.enabled"
+ ),
+ "autoOpen.enabled pref should still be true"
+ );
+ ok(
+ !SpecialPowers.getBoolPref("browser.shopping.experience2023.active"),
+ "Sidebar active pref should be false after pressing auto-open toggle to close the sidebar"
+ );
+
+ // Now try updating the pref directly to see if toggle will change state immediately
+ await SpecialPowers.popPrefEnv();
+ toggleStateChangePromise = ContentTaskUtils.waitForCondition(() => {
+ return autoOpenToggle.hasAttribute("pressed");
+ }, "Waiting for auto-open toggle to be enabled");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.autoOpen.userEnabled", true],
+ ["browser.shopping.experience2023.active", true],
+ ],
+ });
+
+ await toggleStateChangePromise;
+ }
+ );
+
+ BrowserTestUtils.removeTab(tab1);
+
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/shopping/tests/browser/browser_shopping_sidebar.js b/browser/components/shopping/tests/browser/browser_shopping_sidebar.js
new file mode 100644
index 0000000000..31cbc6d732
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_shopping_sidebar.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const SHOPPING_SIDEBAR_WIDTH_PREF =
+ "browser.shopping.experience2023.sidebarWidth";
+
+add_task(async function test_sidebar_opens_correct_size() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["toolkit.shopping.ohttpRelayURL", ""],
+ ["toolkit.shopping.ohttpConfigURL", ""],
+ ["browser.shopping.experience2023.active", true],
+ [SHOPPING_SIDEBAR_WIDTH_PREF, 0],
+ ],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: PRODUCT_TEST_URL,
+ });
+
+ let browserPanel = gBrowser.getPanel(tab.linkedBrowser);
+ let sidebar = browserPanel.querySelector("shopping-sidebar");
+
+ await TestUtils.waitForCondition(() => sidebar.scrollWidth === 320);
+
+ is(sidebar.scrollWidth, 320, "Shopping sidebar should default to 320px");
+
+ let prefChangedPromise = TestUtils.waitForPrefChange(
+ SHOPPING_SIDEBAR_WIDTH_PREF
+ );
+ sidebar.style.width = "345px";
+ await TestUtils.waitForCondition(() => sidebar.scrollWidth === 345);
+ await prefChangedPromise;
+
+ let shoppingButton = document.getElementById("shopping-sidebar-button");
+ shoppingButton.click();
+ await BrowserTestUtils.waitForMutationCondition(
+ shoppingButton,
+ {
+ attributeFilter: ["shoppingsidebaropen"],
+ },
+ () => shoppingButton.getAttribute("shoppingsidebaropen") == "false"
+ );
+
+ shoppingButton.click();
+ await BrowserTestUtils.waitForMutationCondition(
+ shoppingButton,
+ {
+ attributeFilter: ["shoppingsidebaropen"],
+ },
+ () => shoppingButton.getAttribute("shoppingsidebaropen") == "true"
+ );
+
+ await TestUtils.waitForCondition(() => sidebar.scrollWidth === 345);
+
+ is(
+ sidebar.scrollWidth,
+ 345,
+ "Shopping sidebar should open to previous set width of 345"
+ );
+
+ gBrowser.removeTab(tab);
+});
diff --git a/browser/components/shopping/tests/browser/browser_shopping_survey.js b/browser/components/shopping/tests/browser/browser_shopping_survey.js
new file mode 100644
index 0000000000..aebe6e9dcf
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_shopping_survey.js
@@ -0,0 +1,337 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const currentTime = Date.now() / 1000;
+const time25HrsAgo = currentTime - 25 * 60 * 60;
+const time1HrAgo = currentTime - 1 * 60 * 60;
+
+add_task(async function test_setup() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(browser, [], async () => {
+ let childActor = content.windowGlobalChild.getExistingActor(
+ "AboutWelcomeShopping"
+ );
+ childActor.resetChildStates();
+ });
+ }
+ );
+});
+
+/**
+ * Test to check survey renders when show survey conditions are met
+ */
+add_task(async function test_showSurvey_Enabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.optedIn", 1],
+ ["browser.shopping.experience2023.survey.enabled", true],
+ ["browser.shopping.experience2023.survey.hasSeen", false],
+ ["browser.shopping.experience2023.survey.pdpVisits", 5],
+ ["browser.shopping.experience2023.survey.optedInTime", time25HrsAgo],
+ ],
+ });
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [MOCK_ANALYZED_PRODUCT_RESPONSE],
+ async mockData => {
+ const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+ );
+ let surveyPrefChanged = TestUtils.waitForPrefChange(
+ "browser.shopping.experience2023.survey.hasSeen"
+ );
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+
+ // Manually send data update event, as it isn't set due to the lack of mock APIs.
+ // TODO: Support for the mocks will be added in Bug 1853474.
+ let mockObj = {
+ data: mockData,
+ productUrl: "https://example.com/product/1234",
+ };
+ let evt = new content.CustomEvent("Update", {
+ bubbles: true,
+ detail: Cu.cloneInto(mockObj, content),
+ });
+ content.document.dispatchEvent(evt);
+
+ await shoppingContainer.updateComplete;
+ await surveyPrefChanged;
+
+ let childActor = content.windowGlobalChild.getExistingActor(
+ "AboutWelcomeShopping"
+ );
+
+ ok(childActor.surveyEnabled, "Survey is Enabled");
+
+ let surveyScreen = await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.querySelector(
+ "shopping-container .screen.SHOPPING_MICROSURVEY_SCREEN_1"
+ ),
+ "survey-screen"
+ );
+
+ ok(surveyScreen, "Survey screen is rendered");
+
+ ok(
+ childActor.showMicroSurvey,
+ "Show Survey targeting conditions met"
+ );
+ Assert.strictEqual(
+ content.document
+ .getElementById("steps")
+ .getAttribute("data-l10n-id"),
+ "shopping-onboarding-welcome-steps-indicator-label",
+ "Steps indicator has appropriate fluent ID"
+ );
+ ok(
+ !content.document.getElementById("multi-stage-message-root").hidden,
+ "Survey Message container is shown"
+ );
+ ok(
+ content.document.querySelector(".dismiss-button"),
+ "Dismiss button is shown"
+ );
+
+ let survey_seen_status = Services.prefs.getBoolPref(
+ "browser.shopping.experience2023.survey.hasSeen",
+ false
+ );
+ ok(survey_seen_status, "Survey pref state is updated");
+ childActor.resetChildStates();
+ }
+ );
+ }
+ );
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Test to check survey is hidden when survey enabled pref is false
+ */
+add_task(async function test_showSurvey_Disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.optedIn", 1],
+ ["browser.shopping.experience2023.survey.enabled", false],
+ ["browser.shopping.experience2023.survey.hasSeen", false],
+ ["browser.shopping.experience2023.survey.pdpVisits", 5],
+ ["browser.shopping.experience2023.survey.optedInTime", time25HrsAgo],
+ ],
+ });
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [MOCK_ANALYZED_PRODUCT_RESPONSE],
+ async mockData => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+
+ // Manually send data update event, as it isn't set due to the lack of mock APIs.
+ // TODO: Support for the mocks will be added in Bug 1853474.
+ let mockObj = {
+ data: mockData,
+ productUrl: "https://example.com/product/1234",
+ };
+ let evt = new content.CustomEvent("Update", {
+ bubbles: true,
+ detail: Cu.cloneInto(mockObj, content),
+ });
+ content.document.dispatchEvent(evt);
+
+ await shoppingContainer.updateComplete;
+
+ let childActor = content.windowGlobalChild.getExistingActor(
+ "AboutWelcomeShopping"
+ );
+
+ ok(!childActor.surveyEnabled, "Survey is disabled");
+
+ let surveyScreen = content.document.querySelector(
+ "shopping-container .screen.SHOPPING_MICROSURVEY_SCREEN_1"
+ );
+
+ ok(!surveyScreen, "Survey screen is not rendered");
+ ok(
+ !childActor.showMicroSurvey,
+ "Show Survey targeting conditions are not met"
+ );
+ ok(
+ content.document.getElementById("multi-stage-message-root").hidden,
+ "Survey Message container is hidden"
+ );
+
+ childActor.resetChildStates();
+ }
+ );
+ }
+ );
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Test to check survey display logic respects 24 hours after Opt-in rule
+ */
+add_task(async function test_24_hr_since_optin_rule() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.optedIn", 1],
+ ["browser.shopping.experience2023.survey.enabled", true],
+ ["browser.shopping.experience2023.survey.hasSeen", false],
+ ["browser.shopping.experience2023.survey.pdpVisits", 5],
+ ["browser.shopping.experience2023.survey.optedInTime", time1HrAgo],
+ ],
+ });
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [MOCK_ANALYZED_PRODUCT_RESPONSE],
+ async mockData => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+
+ let childActor = content.windowGlobalChild.getExistingActor(
+ "AboutWelcomeShopping"
+ );
+
+ let surveyScreen = content.document.querySelector(
+ "shopping-container .screen.SHOPPING_MICROSURVEY_SCREEN_1"
+ );
+
+ ok(!surveyScreen, "Survey screen is not rendered");
+ ok(
+ !childActor.showMicroSurvey,
+ "Show Survey 24 hours after opt in conditions are not met"
+ );
+ ok(
+ content.document.getElementById("multi-stage-message-root").hidden,
+ "Survey Message container is hidden"
+ );
+
+ childActor.resetChildStates();
+ }
+ );
+ }
+ );
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_confirmation_screen() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.optedIn", 1],
+ ["browser.shopping.experience2023.survey.enabled", true],
+ ["browser.shopping.experience2023.survey.hasSeen", false],
+ ["browser.shopping.experience2023.survey.pdpVisits", 5],
+ ["browser.shopping.experience2023.survey.optedInTime", time25HrsAgo],
+ ],
+ });
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [MOCK_ANALYZED_PRODUCT_RESPONSE],
+ async mockData => {
+ async function clickVisibleElement(selector) {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelector(selector),
+ `waiting for selector ${selector}`,
+ 200, // interval
+ 100 // maxTries
+ );
+ content.document.querySelector(selector).click();
+ }
+
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+
+ // Manually send data update event, as it isn't set due to the lack of mock APIs.
+ // TODO: Support for the mocks will be added in Bug 1853474.
+ let mockObj = {
+ data: mockData,
+ productUrl: "https://example.com/product/1234",
+ };
+ let evt = new content.CustomEvent("Update", {
+ bubbles: true,
+ detail: Cu.cloneInto(mockObj, content),
+ });
+ content.document.dispatchEvent(evt);
+
+ await shoppingContainer.updateComplete;
+
+ let surveyScreen1 = await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.querySelector(
+ "shopping-container .screen.SHOPPING_MICROSURVEY_SCREEN_1"
+ ),
+ "survey-screen"
+ );
+
+ ok(surveyScreen1, "Survey screen 1 is rendered");
+ clickVisibleElement("#radio-1");
+ clickVisibleElement("button.primary");
+
+ let surveyScreen2 = await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.querySelector(
+ "shopping-container .screen.SHOPPING_MICROSURVEY_SCREEN_2"
+ ),
+ "survey-screen"
+ );
+ ok(surveyScreen2, "Survey screen 2 is rendered");
+ clickVisibleElement("#radio-1");
+ clickVisibleElement("button.primary");
+
+ let confirmationScreen = await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelector("shopping-message-bar"),
+ "survey-screen"
+ );
+
+ ok(confirmationScreen, "Survey confirmation screen is rendered");
+ }
+ );
+ }
+ );
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/shopping/tests/browser/browser_shopping_urlbar.js b/browser/components/shopping/tests/browser/browser_shopping_urlbar.js
new file mode 100644
index 0000000000..9eb396e846
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_shopping_urlbar.js
@@ -0,0 +1,427 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const CONTENT_PAGE = "https://example.com";
+const PRODUCT_PAGE = "https://example.com/product/B09TJGHL5F";
+
+add_task(async function test_button_hidden() {
+ await BrowserTestUtils.withNewTab(CONTENT_PAGE, async function (browser) {
+ let shoppingButton = document.getElementById("shopping-sidebar-button");
+ ok(
+ BrowserTestUtils.isHidden(shoppingButton),
+ "Shopping Button should be hidden on a content page"
+ );
+ });
+});
+
+add_task(async function test_button_shown() {
+ await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function (browser) {
+ let shoppingButton = document.getElementById("shopping-sidebar-button");
+ ok(
+ BrowserTestUtils.isVisible(shoppingButton),
+ "Shopping Button should be visible on a product page"
+ );
+ });
+});
+
+// Button is hidden on navigation to a content page
+add_task(async function test_button_changes_with_location() {
+ await BrowserTestUtils.withNewTab(CONTENT_PAGE, async function (browser) {
+ let shoppingButton = document.getElementById("shopping-sidebar-button");
+ ok(
+ BrowserTestUtils.isHidden(shoppingButton),
+ "Shopping Button should be hidden on a content page"
+ );
+ BrowserTestUtils.startLoadingURIString(browser, PRODUCT_PAGE);
+ await BrowserTestUtils.browserLoaded(browser);
+ ok(
+ BrowserTestUtils.isVisible(shoppingButton),
+ "Shopping Button should be visible on a product page"
+ );
+ BrowserTestUtils.startLoadingURIString(browser, CONTENT_PAGE);
+ await BrowserTestUtils.browserLoaded(browser);
+ ok(
+ BrowserTestUtils.isHidden(shoppingButton),
+ "Shopping Button should be hidden on a content page"
+ );
+ });
+});
+
+add_task(async function test_button_active() {
+ Services.prefs.setBoolPref("browser.shopping.experience2023.active", true);
+
+ await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function (browser) {
+ let shoppingButton = document.getElementById("shopping-sidebar-button");
+ Assert.equal(
+ shoppingButton.getAttribute("shoppingsidebaropen"),
+ "true",
+ "Shopping Button should be active when sidebar is open"
+ );
+ });
+});
+
+add_task(async function test_button_inactive() {
+ Services.prefs.setBoolPref("browser.shopping.experience2023.active", false);
+
+ await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function (browser) {
+ let shoppingButton = document.getElementById("shopping-sidebar-button");
+ Assert.equal(
+ shoppingButton.getAttribute("shoppingsidebaropen"),
+ "false",
+ "Shopping Button should be inactive when sidebar is closed"
+ );
+ });
+});
+
+// Switching Tabs shows and hides the button
+add_task(async function test_button_changes_with_tabswitch() {
+ Services.prefs.setBoolPref("browser.shopping.experience2023.active", true);
+
+ let shoppingButton = document.getElementById("shopping-sidebar-button");
+
+ let productTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: PRODUCT_PAGE,
+ });
+ let contentTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: CONTENT_PAGE,
+ });
+
+ await BrowserTestUtils.switchTab(gBrowser, productTab);
+ ok(
+ BrowserTestUtils.isVisible(shoppingButton),
+ "Shopping Button should be visible on a product page"
+ );
+
+ await BrowserTestUtils.switchTab(gBrowser, contentTab);
+ ok(
+ BrowserTestUtils.isHidden(shoppingButton),
+ "Shopping Button should be hidden on a content page"
+ );
+
+ await BrowserTestUtils.removeTab(productTab);
+ await BrowserTestUtils.removeTab(contentTab);
+});
+
+add_task(async function test_button_toggles_sidebars() {
+ Services.prefs.setBoolPref("browser.shopping.experience2023.active", false);
+
+ await BrowserTestUtils.withNewTab(CONTENT_PAGE, async function (browser) {
+ let shoppingButton = document.getElementById("shopping-sidebar-button");
+ let browserPanel = gBrowser.getPanel(browser);
+
+ BrowserTestUtils.startLoadingURIString(browser, PRODUCT_PAGE);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let sidebar = browserPanel.querySelector("shopping-sidebar");
+
+ is(sidebar, null, "Shopping sidebar should be closed");
+
+ // open
+ shoppingButton.click();
+ await BrowserTestUtils.waitForMutationCondition(
+ shoppingButton,
+ {
+ attributeFilter: ["shoppingsidebaropen"],
+ },
+ () => shoppingButton.getAttribute("shoppingsidebaropen") == "true"
+ );
+
+ sidebar = browserPanel.querySelector("shopping-sidebar");
+ ok(BrowserTestUtils.isVisible(sidebar), "Shopping sidebar should be open");
+
+ // close
+ shoppingButton.click();
+ await BrowserTestUtils.waitForMutationCondition(
+ shoppingButton,
+ {
+ attributeFilter: ["shoppingsidebaropen"],
+ },
+ () => shoppingButton.getAttribute("shoppingsidebaropen") == "false"
+ );
+
+ ok(BrowserTestUtils.isHidden(sidebar), "Shopping sidebar should be closed");
+ });
+});
+
+// Button changes all Windows
+add_task(async function test_button_toggles_all_windows() {
+ Services.prefs.setBoolPref("browser.shopping.experience2023.active", false);
+
+ let shoppingButton = document.getElementById("shopping-sidebar-button");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PRODUCT_PAGE);
+
+ let browserPanelA = gBrowser.getPanel(gBrowser.selectedBrowser);
+ let sidebarA = browserPanelA.querySelector("shopping-sidebar");
+
+ let newWindow = await BrowserTestUtils.openNewBrowserWindow();
+
+ BrowserTestUtils.startLoadingURIString(
+ newWindow.gBrowser.selectedBrowser,
+ PRODUCT_PAGE
+ );
+ await BrowserTestUtils.browserLoaded(newWindow.gBrowser.selectedBrowser);
+
+ let browserPanelB = newWindow.gBrowser.getPanel(
+ newWindow.gBrowser.selectedBrowser
+ );
+ let sidebarB = browserPanelB.querySelector("shopping-sidebar");
+
+ is(
+ sidebarA,
+ null,
+ "Shopping sidebar should not exist yet for new tab in current window"
+ );
+ is(sidebarB, null, "Shopping sidebar closed in new window");
+
+ // open
+ shoppingButton.click();
+ await BrowserTestUtils.waitForMutationCondition(
+ shoppingButton,
+ {
+ attributeFilter: ["shoppingsidebaropen"],
+ },
+ () => shoppingButton.getAttribute("shoppingsidebaropen") == "true"
+ );
+ sidebarA = browserPanelA.querySelector("shopping-sidebar");
+ ok(
+ BrowserTestUtils.isVisible(sidebarA),
+ "Shopping sidebar should be open in current window"
+ );
+ sidebarB = browserPanelB.querySelector("shopping-sidebar");
+ ok(
+ BrowserTestUtils.isVisible(sidebarB),
+ "Shopping sidebar should be open in new window"
+ );
+
+ // close
+ shoppingButton.click();
+ await BrowserTestUtils.waitForMutationCondition(
+ shoppingButton,
+ {
+ attributeFilter: ["shoppingsidebaropen"],
+ },
+ () => shoppingButton.getAttribute("shoppingsidebaropen") == "false"
+ );
+
+ ok(
+ BrowserTestUtils.isHidden(sidebarA),
+ "Shopping sidebar should be closed in current window"
+ );
+ ok(
+ BrowserTestUtils.isHidden(sidebarB),
+ "Shopping sidebar should be closed in new window"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(newWindow);
+});
+
+add_task(async function test_button_right_click_doesnt_affect_sidebars() {
+ Services.prefs.setBoolPref("browser.shopping.experience2023.active", false);
+
+ await BrowserTestUtils.withNewTab(CONTENT_PAGE, async function (browser) {
+ let shoppingButton = document.getElementById("shopping-sidebar-button");
+ let browserPanel = gBrowser.getPanel(browser);
+
+ BrowserTestUtils.startLoadingURIString(browser, PRODUCT_PAGE);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let sidebar = browserPanel.querySelector("shopping-sidebar");
+
+ is(sidebar, null, "Shopping sidebar should be closed");
+ EventUtils.synthesizeMouseAtCenter(shoppingButton, { button: 1 });
+ // Wait a tick.
+ await new Promise(executeSoon);
+ sidebar = browserPanel.querySelector("shopping-sidebar");
+ is(sidebar, null, "Shopping sidebar should still be closed");
+ });
+});
+
+add_task(async function test_button_deals_with_tabswitches() {
+ Services.prefs.setBoolPref("browser.shopping.experience2023.active", true);
+
+ await BrowserTestUtils.withNewTab(CONTENT_PAGE, async function (browser) {
+ let shoppingButton = document.getElementById("shopping-sidebar-button");
+
+ ok(
+ BrowserTestUtils.isHidden(shoppingButton),
+ "The shopping button is hidden on a non product page"
+ );
+
+ let newProductTab = BrowserTestUtils.addTab(gBrowser, PRODUCT_PAGE);
+ let newProductBrowser = newProductTab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(
+ newProductBrowser,
+ false,
+ PRODUCT_PAGE
+ );
+
+ ok(
+ BrowserTestUtils.isHidden(shoppingButton),
+ "The shopping button is still hidden after opening a background product tab"
+ );
+
+ let shoppingButtonVisiblePromise =
+ BrowserTestUtils.waitForMutationCondition(
+ shoppingButton,
+ { attributes: true, attributeFilter: ["hidden"] },
+ () => !shoppingButton.hidden
+ );
+ await BrowserTestUtils.switchTab(gBrowser, newProductTab);
+ await shoppingButtonVisiblePromise;
+
+ ok(
+ BrowserTestUtils.isVisible(shoppingButton),
+ "The shopping button is now visible"
+ );
+
+ let newProductTab2 = BrowserTestUtils.addTab(gBrowser, PRODUCT_PAGE);
+ let newProductBrowser2 = newProductTab2.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(
+ newProductBrowser2,
+ false,
+ PRODUCT_PAGE
+ );
+
+ ok(
+ BrowserTestUtils.isVisible(shoppingButton),
+ "The shopping button is still visible after opening background product tab"
+ );
+
+ shoppingButtonVisiblePromise = BrowserTestUtils.waitForMutationCondition(
+ shoppingButton,
+ { attributes: true, attributeFilter: ["hidden"] },
+ () => !shoppingButton.hidden
+ );
+ await BrowserTestUtils.switchTab(gBrowser, newProductTab2);
+ await shoppingButtonVisiblePromise;
+
+ ok(
+ BrowserTestUtils.isVisible(shoppingButton),
+ "The shopping button is still visible"
+ );
+
+ BrowserTestUtils.removeTab(newProductTab2);
+
+ BrowserTestUtils.removeTab(newProductTab);
+ });
+});
+
+add_task(async function test_button_deals_with_tabswitches_post_optout() {
+ Services.prefs.setBoolPref("browser.shopping.experience2023.active", true);
+
+ await BrowserTestUtils.withNewTab(CONTENT_PAGE, async function (browser) {
+ let shoppingButton = document.getElementById("shopping-sidebar-button");
+
+ ok(
+ BrowserTestUtils.isHidden(shoppingButton),
+ "The shopping button is hidden on a non product page"
+ );
+
+ let newProductTab = BrowserTestUtils.addTab(gBrowser, PRODUCT_PAGE);
+ let newProductBrowser = newProductTab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(
+ newProductBrowser,
+ false,
+ PRODUCT_PAGE
+ );
+
+ ok(
+ BrowserTestUtils.isHidden(shoppingButton),
+ "The shopping button is still hidden after opening a background product tab"
+ );
+
+ let shoppingButtonVisiblePromise =
+ BrowserTestUtils.waitForMutationCondition(
+ shoppingButton,
+ { attributes: true, attributeFilter: ["hidden"] },
+ () => !shoppingButton.hidden
+ );
+ await BrowserTestUtils.switchTab(gBrowser, newProductTab);
+ await shoppingButtonVisiblePromise;
+
+ ok(
+ BrowserTestUtils.isVisible(shoppingButton),
+ "The shopping button is now visible"
+ );
+
+ let newProductTab2 = BrowserTestUtils.addTab(gBrowser, PRODUCT_PAGE);
+ let newProductBrowser2 = newProductTab2.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(
+ newProductBrowser2,
+ false,
+ PRODUCT_PAGE
+ );
+
+ ok(
+ BrowserTestUtils.isVisible(shoppingButton),
+ "The shopping button is still visible after opening background product tab"
+ );
+
+ shoppingButtonVisiblePromise = BrowserTestUtils.waitForMutationCondition(
+ shoppingButton,
+ { attributes: true, attributeFilter: ["hidden"] },
+ () => !shoppingButton.hidden
+ );
+ await BrowserTestUtils.switchTab(gBrowser, newProductTab2);
+ await shoppingButtonVisiblePromise;
+
+ ok(
+ BrowserTestUtils.isVisible(shoppingButton),
+ "The shopping button is still visible"
+ );
+
+ // Simulate opt-out
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.active", false],
+ ["browser.shopping.experience2023.optedIn", 2],
+ ],
+ });
+
+ ok(
+ BrowserTestUtils.isVisible(shoppingButton),
+ "The shopping button is still visible after opting out."
+ );
+ Assert.equal(
+ shoppingButton.getAttribute("shoppingsidebaropen"),
+ "false",
+ "Button not marked as open."
+ );
+
+ // Switch to non-product tab.
+ await BrowserTestUtils.switchTab(
+ gBrowser,
+ gBrowser.getTabForBrowser(browser)
+ );
+ ok(
+ BrowserTestUtils.isHidden(shoppingButton),
+ "The shopping button is hidden on non-product page."
+ );
+ Assert.equal(
+ shoppingButton.getAttribute("shoppingsidebaropen"),
+ "false",
+ "Button not marked as open."
+ );
+ // Switch to non-product tab.
+ await BrowserTestUtils.switchTab(gBrowser, newProductTab);
+ ok(
+ BrowserTestUtils.isVisible(shoppingButton),
+ "The shopping button is still visible on a different product tab after opting out."
+ );
+ Assert.equal(
+ shoppingButton.getAttribute("shoppingsidebaropen"),
+ "false",
+ "Button not marked as open."
+ );
+
+ BrowserTestUtils.removeTab(newProductTab2);
+
+ BrowserTestUtils.removeTab(newProductTab);
+ });
+});
diff --git a/browser/components/shopping/tests/browser/browser_stale_product.js b/browser/components/shopping/tests/browser/browser_stale_product.js
new file mode 100644
index 0000000000..45bed46a6b
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_stale_product.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the correct shopping-message-bar component appears if a product analysis is stale.
+ * Other analysis details should be visible.
+ */
+add_task(async function test_stale_product() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ let shoppingContainer = await getAnalysisDetails(
+ browser,
+ MOCK_STALE_PRODUCT_RESPONSE
+ );
+
+ ok(
+ shoppingContainer.shoppingMessageBarEl,
+ "Got shopping-message-bar element"
+ );
+ is(
+ shoppingContainer.shoppingMessageBarType,
+ "stale",
+ "shopping-message-bar type should be correct"
+ );
+
+ verifyAnalysisDetailsVisible(shoppingContainer);
+ verifyFooterVisible(shoppingContainer);
+ }
+ );
+});
diff --git a/browser/components/shopping/tests/browser/browser_ui_telemetry.js b/browser/components/shopping/tests/browser/browser_ui_telemetry.js
new file mode 100644
index 0000000000..b97aca1963
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_ui_telemetry.js
@@ -0,0 +1,762 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const CONTENT_PAGE = "https://example.com";
+const PRODUCT_PAGE = "https://example.com/product/B09TJGHL5F";
+
+function assertEventMatches(gleanEvent, requiredValues) {
+ let limitedEvent = Object.assign({}, gleanEvent);
+ for (let k of Object.keys(limitedEvent)) {
+ if (!requiredValues.hasOwnProperty(k)) {
+ delete limitedEvent[k];
+ }
+ }
+ return Assert.deepEqual(limitedEvent, requiredValues);
+}
+
+add_task(async function test_shopping_reanalysis_event() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.shopping.experience2023.active", true]],
+ });
+
+ // testFlushAllChildren() is necessary to deal with the event being
+ // recorded in content, but calling testGetValue() in parent.
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await clickReAnalyzeLink(browser, MOCK_STALE_PRODUCT_RESPONSE);
+ }
+ );
+
+ await Services.fog.testFlushAllChildren();
+ var staleAnalysisEvents =
+ Glean.shopping.surfaceStaleAnalysisShown.testGetValue();
+
+ assertEventMatches(staleAnalysisEvents[0], {
+ category: "shopping",
+ name: "surface_stale_analysis_shown",
+ });
+
+ var reanalysisRequestedEvents =
+ Glean.shopping.surfaceReanalyzeClicked.testGetValue();
+
+ assertEventMatches(reanalysisRequestedEvents[0], {
+ category: "shopping",
+ name: "surface_reanalyze_clicked",
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_reactivated_product_button_click() {
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await clickProductAvailableLink(browser, MOCK_STALE_PRODUCT_RESPONSE);
+ }
+ );
+
+ await Services.fog.testFlushAllChildren();
+
+ var reanalysisEvents =
+ Glean.shopping.surfaceReactivatedButtonClicked.testGetValue();
+ assertEventMatches(reanalysisEvents[0], {
+ category: "shopping",
+ name: "surface_reactivated_button_clicked",
+ });
+});
+
+add_task(async function test_no_reliability_available_request_click() {
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await clickCheckReviewQualityButton(
+ browser,
+ MOCK_UNANALYZED_PRODUCT_RESPONSE
+ );
+ }
+ );
+
+ await Services.fog.testFlushAllChildren();
+ var requestEvents =
+ Glean.shopping.surfaceAnalyzeReviewsNoneAvailableClicked.testGetValue();
+
+ assertEventMatches(requestEvents[0], {
+ category: "shopping",
+ name: "surface_analyze_reviews_none_available_clicked",
+ });
+});
+
+add_task(async function test_shopping_sidebar_displayed() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.shopping.experience2023.active", true]],
+ });
+
+ Services.fog.testResetFOG();
+
+ await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function (browser) {
+ let shoppingButton = document.getElementById("shopping-sidebar-button");
+ await BrowserTestUtils.waitForMutationCondition(
+ shoppingButton,
+ {
+ attributeFilter: ["shoppingsidebaropen"],
+ },
+ () => shoppingButton.getAttribute("shoppingsidebaropen") == "true"
+ );
+ let sidebar = gBrowser.getPanel(browser).querySelector("shopping-sidebar");
+ Assert.ok(
+ BrowserTestUtils.isVisible(sidebar),
+ "Sidebar should be visible."
+ );
+
+ // open a new tab onto a page where sidebar is not visible.
+ let contentTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: CONTENT_PAGE,
+ });
+
+ // change the focused tab a few times to ensure we don't increment on tab
+ // switch.
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]);
+ await BrowserTestUtils.switchTab(gBrowser, contentTab);
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]);
+
+ BrowserTestUtils.removeTab(contentTab);
+ });
+
+ await Services.fog.testFlushAllChildren();
+
+ var displayedEvents = Glean.shopping.surfaceDisplayed.testGetValue();
+ Assert.equal(1, displayedEvents.length);
+ assertEventMatches(displayedEvents[0], {
+ category: "shopping",
+ name: "surface_displayed",
+ });
+
+ var addressBarIconDisplayedEvents =
+ Glean.shopping.addressBarIconDisplayed.testGetValue();
+ assertEventMatches(addressBarIconDisplayedEvents[0], {
+ category: "shopping",
+ name: "address_bar_icon_displayed",
+ });
+
+ // reset FOG and check a page that should NOT have these events
+ Services.fog.testResetFOG();
+
+ await BrowserTestUtils.withNewTab(CONTENT_PAGE, async function (browser) {
+ let sidebar = gBrowser.getPanel(browser).querySelector("shopping-sidebar");
+
+ Assert.equal(sidebar, null);
+ });
+
+ var emptyDisplayedEvents = Glean.shopping.surfaceDisplayed.testGetValue();
+ var emptyAddressBarIconDisplayedEvents =
+ Glean.shopping.addressBarIconDisplayed.testGetValue();
+
+ Assert.equal(emptyDisplayedEvents, null);
+ Assert.equal(emptyAddressBarIconDisplayedEvents, null);
+
+ // Open a product page in a background tab, verify telemetry is not recorded.
+ let backgroundTab = await BrowserTestUtils.addTab(gBrowser, PRODUCT_PAGE);
+ await Services.fog.testFlushAllChildren();
+ let tabSwitchEvents = Glean.shopping.surfaceDisplayed.testGetValue();
+ Assert.equal(tabSwitchEvents, null);
+ Services.fog.testResetFOG();
+
+ // Next, switch tabs to the backgrounded product tab and verify telemetry is
+ // recorded.
+ await BrowserTestUtils.switchTab(gBrowser, backgroundTab);
+ await Services.fog.testFlushAllChildren();
+ tabSwitchEvents = Glean.shopping.surfaceDisplayed.testGetValue();
+ Assert.equal(1, tabSwitchEvents.length);
+ assertEventMatches(tabSwitchEvents[0], {
+ category: "shopping",
+ name: "surface_displayed",
+ });
+ Services.fog.testResetFOG();
+
+ // Finally, switch tabs again and verify telemetry is not recorded for the
+ // background tab after it has been foregrounded once.
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]);
+ await BrowserTestUtils.switchTab(gBrowser, backgroundTab);
+ await Services.fog.testFlushAllChildren();
+ tabSwitchEvents = Glean.shopping.surfaceDisplayed.testGetValue();
+ Assert.equal(tabSwitchEvents, null);
+ Services.fog.testResetFOG();
+ BrowserTestUtils.removeTab(backgroundTab);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_shopping_card_clicks() {
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await clickShowMoreButton(browser, MOCK_ANALYZED_PRODUCT_RESPONSE);
+ }
+ );
+
+ await Services.fog.testFlushAllChildren();
+ var learnMoreButtonEvents =
+ Glean.shopping.surfaceShowMoreReviewsButtonClicked.testGetValue();
+
+ assertEventMatches(learnMoreButtonEvents[0], {
+ category: "shopping",
+ name: "surface_show_more_reviews_button_clicked",
+ });
+});
+
+add_task(async function test_close_telemetry_recorded() {
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await clickCloseButton(browser, MOCK_ANALYZED_PRODUCT_RESPONSE);
+ }
+ );
+
+ await Services.fog.testFlushAllChildren();
+
+ var closeEvents = Glean.shopping.surfaceClosed.testGetValue();
+ assertEventMatches(closeEvents[0], {
+ category: "shopping",
+ name: "surface_closed",
+ extra: { source: "closeButton" },
+ });
+
+ // Ensure that the sidebar is open so we confirm the icon click closes it.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.shopping.experience2023.active", true]],
+ });
+
+ await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function (browser) {
+ let shoppingButton = document.getElementById("shopping-sidebar-button");
+ shoppingButton.click();
+ });
+
+ await Services.fog.testFlushAllChildren();
+ var urlBarIconEvents = Glean.shopping.addressBarIconClicked.testGetValue();
+ assertEventMatches(urlBarIconEvents[0], {
+ category: "shopping",
+ name: "address_bar_icon_clicked",
+ extra: { action: "closed" },
+ });
+
+ var closeSurfaceEvents = Glean.shopping.surfaceClosed.testGetValue();
+ assertEventMatches(closeSurfaceEvents[0], {
+ category: "shopping",
+ name: "surface_closed",
+ extra: { source: "closeButton" },
+ });
+
+ assertEventMatches(closeSurfaceEvents[1], {
+ category: "shopping",
+ name: "surface_closed",
+ extra: { source: "addressBarIcon" },
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_powered_by_fakespot_link() {
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await clickPoweredByFakespotLink(browser, MOCK_ANALYZED_PRODUCT_RESPONSE);
+ }
+ );
+
+ await Services.fog.testFlushAllChildren();
+
+ let fakespotLinkEvents =
+ Glean.shopping.surfacePoweredByFakespotLinkClicked.testGetValue();
+ assertEventMatches(fakespotLinkEvents[0], {
+ category: "shopping",
+ name: "surface_powered_by_fakespot_link_clicked",
+ });
+});
+
+add_task(async function test_review_quality_explainer_link() {
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await clickReviewQualityExplainerLink(
+ browser,
+ MOCK_ANALYZED_PRODUCT_RESPONSE
+ );
+ }
+ );
+
+ await Services.fog.testFlushAllChildren();
+
+ let qualityExplainerEvents =
+ Glean.shopping.surfaceShowQualityExplainerUrlClicked.testGetValue();
+ assertEventMatches(qualityExplainerEvents[0], {
+ category: "shopping",
+ name: "surface_show_quality_explainer_url_clicked",
+ });
+});
+
+// Start with ads user enabled, then disable them, and verify telemetry.
+add_task(async function test_ads_disable_button_click() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.adsEnabled", true],
+ ["browser.shopping.experience2023.ads.userEnabled", true],
+ ],
+ });
+
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ let mockArgs = {
+ mockData: MOCK_ANALYZED_PRODUCT_RESPONSE,
+ mockRecommendationData: MOCK_RECOMMENDED_ADS_RESPONSE,
+ };
+
+ await clickAdsToggle(browser, mockArgs);
+
+ await Services.fog.testFlushAllChildren();
+
+ // Verify the ads state was changed to disabled.
+ let toggledEvents =
+ Glean.shopping.surfaceAdsSettingToggled.testGetValue();
+ assertEventMatches(toggledEvents[0], {
+ category: "shopping",
+ name: "surface_ads_setting_toggled",
+ extra: { action: "disabled" },
+ });
+
+ // Verify the ads disabled state is set to true.
+ Assert.equal(
+ Glean.shoppingSettings.disabledAds.testGetValue(),
+ true,
+ "Ads should be marked as disabled"
+ );
+ }
+ );
+});
+
+// Start with ads user disabled, then enable them, and verify telemetry.
+add_task(async function test_ads_enable_button_click() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.adsEnabled", true],
+ ["browser.shopping.experience2023.ads.userEnabled", false],
+ ],
+ });
+
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ let mockArgs = {
+ mockData: MOCK_ANALYZED_PRODUCT_RESPONSE,
+ mockRecommendationData: MOCK_RECOMMENDED_ADS_RESPONSE,
+ };
+
+ await clickAdsToggle(browser, mockArgs);
+
+ await Services.fog.testFlushAllChildren();
+
+ // Verify the ads state was changed to enabled.
+ let toggledEvents =
+ Glean.shopping.surfaceAdsSettingToggled.testGetValue();
+ assertEventMatches(toggledEvents[0], {
+ category: "shopping",
+ name: "surface_ads_setting_toggled",
+ extra: { action: "enabled" },
+ });
+
+ // Verify the ads disabled state is set to false.
+ Assert.equal(
+ Glean.shoppingSettings.disabledAds.testGetValue(),
+ false,
+ "Ads should be marked as enabled"
+ );
+ }
+ );
+});
+
+add_task(async function test_auto_open_settings_toggle() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.autoOpen.enabled", true],
+ ["browser.shopping.experience2023.autoOpen.userEnabled", true],
+ ],
+ });
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ let mockData = MOCK_ANALYZED_PRODUCT_RESPONSE;
+ await clickAutoOpenToggle(browser, mockData);
+ await Services.fog.testFlushAllChildren();
+ let toggledEvents =
+ Glean.shopping.surfaceAutoOpenSettingToggled.testGetValue();
+ assertEventMatches(toggledEvents[0], {
+ category: "shopping",
+ name: "surface_auto_open_setting_toggled",
+ extra: { action: "disabled" },
+ });
+
+ Services.fog.testResetFOG();
+
+ // Toggle back in the other direction.
+ await clickAutoOpenToggle(browser, mockData);
+ await Services.fog.testFlushAllChildren();
+ toggledEvents =
+ Glean.shopping.surfaceAutoOpenSettingToggled.testGetValue();
+ assertEventMatches(toggledEvents[0], {
+ category: "shopping",
+ name: "surface_auto_open_setting_toggled",
+ extra: { action: "enabled" },
+ });
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_auto_open_no_thanks_button_click() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.autoOpen.enabled", true],
+ ["browser.shopping.experience2023.autoOpen.userEnabled", true],
+ ["browser.shopping.experience2023.showKeepSidebarClosedMessage", true],
+ ],
+ });
+
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ let mockArgs = {
+ mockData: MOCK_ANALYZED_PRODUCT_RESPONSE,
+ };
+
+ await clickNoThanksButton(browser, mockArgs);
+
+ await Services.fog.testFlushAllChildren();
+
+ let noThanksButtonEvents =
+ Glean.shopping.surfaceNoThanksButtonClicked.testGetValue();
+
+ assertEventMatches(noThanksButtonEvents[0], {
+ category: "shopping",
+ name: "surface_no_thanks_button_clicked",
+ });
+ }
+ );
+});
+
+add_task(async function test_auto_open_yes_keep_closed_button() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.autoOpen.enabled", true],
+ ["browser.shopping.experience2023.autoOpen.userEnabled", true],
+ ["browser.shopping.experience2023.showKeepSidebarClosedMessage", true],
+ ],
+ });
+
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ let mockArgs = {
+ mockData: MOCK_ANALYZED_PRODUCT_RESPONSE,
+ };
+
+ await clickYesKeepClosedButton(browser, mockArgs);
+
+ await Services.fog.testFlushAllChildren();
+
+ let yesKeepClosedButtonEvents =
+ Glean.shopping.surfaceYesKeepClosedButtonClicked.testGetValue();
+
+ assertEventMatches(yesKeepClosedButtonEvents[0], {
+ category: "shopping",
+ name: "surface_yes_keep_closed_button_clicked",
+ });
+ }
+ );
+});
+
+add_task(async function test_auto_open_user_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.shopping.experience2023.autoOpen.enabled", true],
+ ["browser.shopping.experience2023.autoOpen.userEnabled", true],
+ ],
+ });
+
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+
+ Services.prefs.setBoolPref(
+ "browser.shopping.experience2023.autoOpen.userEnabled",
+ false
+ );
+
+ await Services.fog.testFlushAllChildren();
+
+ Assert.equal(
+ Glean.shoppingSettings.autoOpenUserDisabled.testGetValue(),
+ true,
+ "Auto open should be marked as disabled"
+ );
+});
+
+function clickAdsToggle(browser, data) {
+ return SpecialPowers.spawn(browser, [data], async args => {
+ const { mockData, mockRecommendationData } = args;
+ let shoppingContainer =
+ content.document.querySelector("shopping-container").wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ shoppingContainer.recommendationData = Cu.cloneInto(
+ mockRecommendationData,
+ content
+ );
+
+ await shoppingContainer.updateComplete;
+
+ let shoppingSettings = shoppingContainer.settingsEl;
+ let toggle = shoppingSettings.recommendationsToggleEl;
+ toggle.click();
+
+ await shoppingContainer.updateComplete;
+ });
+}
+
+function clickAutoOpenToggle(browser, data) {
+ return SpecialPowers.spawn(browser, [data], async mockData => {
+ let shoppingContainer =
+ content.document.querySelector("shopping-container").wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+
+ await shoppingContainer.updateComplete;
+
+ let shoppingSettings = shoppingContainer.settingsEl;
+ let toggle = shoppingSettings.autoOpenToggleEl;
+ toggle.click();
+
+ await shoppingContainer.updateComplete;
+ });
+}
+
+function clickReAnalyzeLink(browser, data) {
+ return SpecialPowers.spawn(browser, [data], async mockData => {
+ let shoppingContainer =
+ content.document.querySelector("shopping-container").wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+
+ let shoppingMessageBar = shoppingContainer.shoppingMessageBarEl;
+ await shoppingMessageBar.updateComplete;
+
+ await shoppingMessageBar.onClickAnalysisButton();
+
+ return "clicked";
+ });
+}
+
+function clickCloseButton(browser, data) {
+ return SpecialPowers.spawn(browser, [data], async mockData => {
+ let shoppingContainer =
+ content.document.querySelector("shopping-container").wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+
+ let closeButton =
+ shoppingContainer.shadowRoot.querySelector("#close-button");
+ await closeButton.updateComplete;
+
+ closeButton.click();
+ });
+}
+
+function clickProductAvailableLink(browser, data) {
+ return SpecialPowers.spawn(browser, [data], async mockData => {
+ let shoppingContainer =
+ content.document.querySelector("shopping-container").wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+
+ let shoppingMessageBar = shoppingContainer.shoppingMessageBarEl;
+ await shoppingMessageBar.updateComplete;
+
+ // calling onClickProductAvailable will fail quietly in cases where this is
+ // not possible to call, so assure it exists first.
+ Assert.notEqual(shoppingMessageBar, null);
+ await shoppingMessageBar.onClickProductAvailable();
+ });
+}
+
+function clickShowMoreButton(browser, data) {
+ return SpecialPowers.spawn(browser, [data], async mockData => {
+ let shoppingContainer =
+ content.document.querySelector("shopping-container").wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+
+ let highlights = shoppingContainer.highlightsEl;
+ let card = highlights.shadowRoot.querySelector("shopping-card");
+ let button = card.shadowRoot.querySelector("article footer button");
+
+ button.click();
+ });
+}
+
+function clickCheckReviewQualityButton(browser, data) {
+ return SpecialPowers.spawn(browser, [data], async mockData => {
+ let shoppingContainer =
+ content.document.querySelector("shopping-container").wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+
+ let button = shoppingContainer.unanalyzedProductEl.shadowRoot
+ .querySelector("shopping-card")
+ .querySelector("button");
+
+ button.click();
+ });
+}
+
+function clickPoweredByFakespotLink(browser, data) {
+ return SpecialPowers.spawn(browser, [data], async mockData => {
+ let shoppingContainer =
+ content.document.querySelector("shopping-container").wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+
+ let settingsEl = shoppingContainer.settingsEl;
+ await settingsEl.updateComplete;
+ let fakespotLink = settingsEl.fakespotLearnMoreLinkEl;
+
+ // Prevent link navigation for test.
+ fakespotLink.href = undefined;
+ await fakespotLink.updateComplete;
+
+ fakespotLink.click();
+ });
+}
+
+function clickReviewQualityExplainerLink(browser, data) {
+ return SpecialPowers.spawn(browser, [data], async mockData => {
+ let shoppingContainer =
+ content.document.querySelector("shopping-container").wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+
+ let analysisExplainerEl = shoppingContainer.analysisExplainerEl;
+ await analysisExplainerEl.updateComplete;
+ let reviewQualityLink = analysisExplainerEl.reviewQualityExplainerLink;
+
+ // Prevent link navigation for test.
+ reviewQualityLink.href = undefined;
+ await reviewQualityLink.updateComplete;
+
+ reviewQualityLink.click();
+ });
+}
+
+function clickNoThanksButton(browser, data) {
+ return SpecialPowers.spawn(browser, [data], async mockData => {
+ let shoppingContainer =
+ content.document.querySelector("shopping-container").wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ // Force the "keep closed" to appear
+ shoppingContainer.showingKeepClosedMessage = true;
+ await shoppingContainer.updateComplete;
+
+ let shoppingMessageBar = shoppingContainer.keepClosedMessageBarEl;
+ await shoppingMessageBar.updateComplete;
+
+ let button = shoppingMessageBar.noThanksButtonEl;
+ button.click();
+ });
+}
+
+function clickYesKeepClosedButton(browser, data) {
+ return SpecialPowers.spawn(browser, [data], async mockData => {
+ let shoppingContainer =
+ content.document.querySelector("shopping-container").wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ // Force the "keep closed" to appear
+ shoppingContainer.showingKeepClosedMessage = true;
+ await shoppingContainer.updateComplete;
+
+ let shoppingMessageBar = shoppingContainer.keepClosedMessageBarEl;
+ await shoppingMessageBar.updateComplete;
+
+ let button = shoppingMessageBar.yesKeepClosedButtonEl;
+ button.click();
+ });
+}
diff --git a/browser/components/shopping/tests/browser/browser_unanalyzed_product.js b/browser/components/shopping/tests/browser/browser_unanalyzed_product.js
new file mode 100644
index 0000000000..a28611cdbf
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_unanalyzed_product.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the unanalyzed product card appears if a product has no analysis yet.
+ * Settings should be the only other component that is visible.
+ */
+add_task(async function test_unanalyzed_product() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ let shoppingContainer = await getAnalysisDetails(
+ browser,
+ MOCK_UNANALYZED_PRODUCT_RESPONSE
+ );
+
+ ok(
+ shoppingContainer.unanalyzedProductEl,
+ "Got the unanalyzed-product-card element"
+ );
+
+ verifyAnalysisDetailsHidden(shoppingContainer);
+ verifyFooterVisible(shoppingContainer);
+ }
+ );
+});
+
+/**
+ * Tests that the unanalyzed product card is hidden if a product already has an up-to-date analysis.
+ * Other analysis details should be visible.
+ */
+add_task(async function test_analyzed_product() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ let shoppingContainer = await getAnalysisDetails(
+ browser,
+ MOCK_ANALYZED_PRODUCT_RESPONSE
+ );
+
+ ok(
+ !shoppingContainer.unanalyzedProductEl,
+ "unanalyzed-product-card should not be visible"
+ );
+
+ verifyAnalysisDetailsVisible(shoppingContainer);
+ verifyFooterVisible(shoppingContainer);
+ }
+ );
+});
+
+/**
+ * Tests that the unanalyzed product card appears if a product has no grade,
+ * even if a product id is available.
+ * Settings should be the only other component that is visible.
+ */
+add_task(async function test_ungraded_product() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ let shoppingContainer = await getAnalysisDetails(
+ browser,
+ MOCK_UNGRADED_PRODUCT_RESPONSE
+ );
+
+ ok(
+ shoppingContainer.unanalyzedProductEl,
+ "Got the unanalyzed-product-card element"
+ );
+
+ verifyAnalysisDetailsHidden(shoppingContainer);
+ verifyFooterVisible(shoppingContainer);
+ }
+ );
+});
diff --git a/browser/components/shopping/tests/browser/browser_unavailable_product.js b/browser/components/shopping/tests/browser/browser_unavailable_product.js
new file mode 100644
index 0000000000..96f82ae296
--- /dev/null
+++ b/browser/components/shopping/tests/browser/browser_unavailable_product.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the correct shopping-message-bar component appears if a product was marked as unavailable.
+ */
+add_task(async function test_unavailable_product() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [MOCK_UNAVAILABLE_PRODUCT_RESPONSE],
+ async mockData => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+
+ let productNotAvailableMessageBar =
+ shoppingContainer.shoppingMessageBarEl;
+
+ ok(productNotAvailableMessageBar, "Got shopping-message-bar element");
+ is(
+ productNotAvailableMessageBar?.getAttribute("type"),
+ "product-not-available",
+ "shopping-message-bar type should be correct"
+ );
+
+ let productAvailableBtn =
+ productNotAvailableMessageBar?.productAvailableBtnEl;
+
+ ok(productAvailableBtn, "Got report product available button");
+
+ let thanksForReportMessageBarVisible =
+ ContentTaskUtils.waitForCondition(() => {
+ return (
+ !!shoppingContainer.shoppingMessageBarEl &&
+ ContentTaskUtils.isVisible(
+ shoppingContainer.shoppingMessageBarEl
+ )
+ );
+ }, "Waiting for shopping-message-bar to be visible");
+
+ productAvailableBtn.click();
+
+ await thanksForReportMessageBarVisible;
+
+ is(
+ shoppingContainer.shoppingMessageBarEl?.getAttribute("type"),
+ "thanks-for-reporting",
+ "shopping-message-bar type should be correct"
+ );
+ }
+ );
+ }
+ );
+});
+
+/**
+ * Tests that the correct shopping-message-bar component appears if a product marked as unavailable
+ * was reported to be back in stock by another user.
+ */
+add_task(async function test_unavailable_product_reported() {
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "about:shoppingsidebar",
+ gBrowser,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [MOCK_UNAVAILABLE_PRODUCT_REPORTED_RESPONSE],
+ async mockData => {
+ let shoppingContainer =
+ content.document.querySelector(
+ "shopping-container"
+ ).wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+
+ ok(
+ shoppingContainer.shoppingMessageBarEl,
+ "Got shopping-message-bar element"
+ );
+ is(
+ shoppingContainer.shoppingMessageBarEl?.getAttribute("type"),
+ "product-not-available-reported",
+ "shopping-message-bar type should be correct"
+ );
+ }
+ );
+ }
+ );
+});
diff --git a/browser/components/shopping/tests/browser/head.js b/browser/components/shopping/tests/browser/head.js
new file mode 100644
index 0000000000..49367fd58b
--- /dev/null
+++ b/browser/components/shopping/tests/browser/head.js
@@ -0,0 +1,225 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/shopping/test/browser/head.js",
+ this
+);
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const MOCK_UNPOPULATED_DATA = {
+ adjusted_rating: null,
+ grade: null,
+ highlights: null,
+};
+
+const MOCK_POPULATED_DATA = {
+ adjusted_rating: 5,
+ grade: "B",
+ highlights: {
+ price: {
+ positive: ["This watch is great and the price was even better."],
+ negative: [],
+ neutral: [],
+ },
+ quality: {
+ positive: [
+ "Other than that, I am very impressed with the watch and it’s capabilities.",
+ "This watch performs above expectations in every way with the exception of the heart rate monitor.",
+ ],
+ negative: [
+ "Battery life is no better than the 3 even with the solar gimmick, probably worse.",
+ ],
+ neutral: [
+ "I have small wrists and still went with the 6X and glad I did.",
+ "I can deal with the looks, as Im now retired.",
+ ],
+ },
+ competitiveness: {
+ positive: [
+ "Bought this to replace my vivoactive 3.",
+ "I like that this watch has so many features, especially those that monitor health like SP02, respiration, sleep, HRV status, stress, and heart rate.",
+ ],
+ negative: [
+ "I do not use it for sleep or heartrate monitoring so not sure how accurate they are.",
+ ],
+ neutral: [
+ "I've avoided getting a smartwatch for so long due to short battery life on most of them.",
+ ],
+ },
+ "packaging/appearance": {
+ positive: ["Great cardboard box."],
+ negative: [],
+ neutral: [],
+ },
+ shipping: {
+ positive: [],
+ negative: [],
+ neutral: [],
+ },
+ },
+};
+
+const MOCK_INVALID_KEY_OBJ = {
+ invalidHighlight: {
+ negative: ["This is an invalid highlight and should not be visible"],
+ },
+ shipping: {
+ positive: [],
+ negative: [],
+ neutral: [],
+ },
+};
+
+const MOCK_UNANALYZED_PRODUCT_RESPONSE = {
+ ...MOCK_UNPOPULATED_DATA,
+ product_id: null,
+ needs_analysis: true,
+};
+
+const MOCK_STALE_PRODUCT_RESPONSE = {
+ ...MOCK_POPULATED_DATA,
+ product_id: "ABCD123",
+ grade: "A",
+ needs_analysis: true,
+};
+
+const MOCK_UNGRADED_PRODUCT_RESPONSE = {
+ ...MOCK_UNPOPULATED_DATA,
+ product_id: "ABCD123",
+ needs_analysis: true,
+};
+
+const MOCK_NOT_ENOUGH_REVIEWS_PRODUCT_RESPONSE = {
+ ...MOCK_UNPOPULATED_DATA,
+ product_id: "ABCD123",
+ needs_analysis: false,
+ not_enough_reviews: true,
+};
+
+const MOCK_ANALYZED_PRODUCT_RESPONSE = {
+ ...MOCK_POPULATED_DATA,
+ product_id: "ABCD123",
+ needs_analysis: false,
+};
+
+const MOCK_UNAVAILABLE_PRODUCT_RESPONSE = {
+ ...MOCK_POPULATED_DATA,
+ product_id: "ABCD123",
+ deleted_product: true,
+};
+
+const MOCK_UNAVAILABLE_PRODUCT_REPORTED_RESPONSE = {
+ ...MOCK_UNAVAILABLE_PRODUCT_RESPONSE,
+ deleted_product_reported: true,
+};
+
+const MOCK_PAGE_NOT_SUPPORTED_RESPONSE = {
+ ...MOCK_UNPOPULATED_DATA,
+ page_not_supported: true,
+};
+
+const MOCK_RECOMMENDED_ADS_RESPONSE = [
+ {
+ name: "VIVO Electric 60 x 24 inch Stand Up Desk | Black Table Top, Black Frame, Height Adjustable Standing Workstation with Memory Preset Controller (DESK-KIT-1B6B)",
+ url: "www.example.com",
+ price: "249.99",
+ currency: "USD",
+ grade: "A",
+ adjusted_rating: 4.6,
+ sponsored: true,
+ image_blob: new Blob(new Uint8Array(), { type: "image/jpeg" }),
+ },
+];
+
+function verifyAnalysisDetailsVisible(shoppingContainer) {
+ ok(
+ shoppingContainer.reviewReliabilityEl,
+ "review-reliability should be visible"
+ );
+ ok(shoppingContainer.adjustedRatingEl, "adjusted-rating should be visible");
+ ok(shoppingContainer.highlightsEl, "review-highlights should be visible");
+}
+
+function verifyAnalysisDetailsHidden(shoppingContainer) {
+ ok(
+ !shoppingContainer.reviewReliabilityEl,
+ "review-reliability should not be visible"
+ );
+ ok(
+ !shoppingContainer.adjustedRatingEl,
+ "adjusted-rating should not be visible"
+ );
+ ok(
+ !shoppingContainer.highlightsEl,
+ "review-highlights should not be visible"
+ );
+}
+
+function verifyFooterVisible(shoppingContainer) {
+ ok(shoppingContainer.settingsEl, "Got the shopping-settings element");
+ ok(
+ shoppingContainer.analysisExplainerEl,
+ "Got the analysis-explainer element"
+ );
+}
+
+function verifyFooterHidden(shoppingContainer) {
+ ok(!shoppingContainer.settingsEl, "Do not render shopping-settings element");
+ ok(
+ !shoppingContainer.analysisExplainerEl,
+ "Do not render the analysis-explainer element"
+ );
+}
+
+function getAnalysisDetails(browser, data) {
+ return SpecialPowers.spawn(browser, [data], async mockData => {
+ let shoppingContainer =
+ content.document.querySelector("shopping-container").wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+ let returnState = {};
+ for (let el of [
+ "unanalyzedProductEl",
+ "reviewReliabilityEl",
+ "analysisExplainerEl",
+ "adjustedRatingEl",
+ "highlightsEl",
+ "settingsEl",
+ "shoppingMessageBarEl",
+ "loadingEl",
+ ]) {
+ returnState[el] =
+ !!shoppingContainer[el] &&
+ ContentTaskUtils.isVisible(shoppingContainer[el]);
+ }
+ returnState.shoppingMessageBarType =
+ shoppingContainer.shoppingMessageBarEl?.getAttribute("type");
+ returnState.isOffline = shoppingContainer.isOffline;
+ return returnState;
+ });
+}
+
+function getSettingsDetails(browser, data) {
+ return SpecialPowers.spawn(browser, [data], async mockData => {
+ let shoppingContainer =
+ content.document.querySelector("shopping-container").wrappedJSObject;
+ shoppingContainer.data = Cu.cloneInto(mockData, content);
+ await shoppingContainer.updateComplete;
+ let shoppingSettings = shoppingContainer.settingsEl;
+ await shoppingSettings.updateComplete;
+ let returnState = {
+ settingsEl:
+ !!shoppingSettings && ContentTaskUtils.isVisible(shoppingSettings),
+ };
+ for (let el of ["recommendationsToggleEl", "optOutButtonEl"]) {
+ returnState[el] =
+ !!shoppingSettings[el] &&
+ ContentTaskUtils.isVisible(shoppingSettings[el]);
+ }
+ return returnState;
+ });
+}