diff options
Diffstat (limited to 'browser/components/shopping/tests')
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; + }); +} |