diff options
Diffstat (limited to 'browser/components/newtab/test/browser')
51 files changed, 4805 insertions, 0 deletions
diff --git a/browser/components/newtab/test/browser/abouthomecache/browser.toml b/browser/components/newtab/test/browser/abouthomecache/browser.toml new file mode 100644 index 0000000000..1994415d9a --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser.toml @@ -0,0 +1,52 @@ +[DEFAULT] +support-files = [ + "head.js", + "../topstories.json", +] +prefs = [ + "browser.tabs.remote.separatePrivilegedContentProcess=true", + "browser.startup.homepage.abouthome_cache.enabled=true", + "browser.startup.homepage.abouthome_cache.cache_on_shutdown=false", + "browser.startup.homepage.abouthome_cache.loglevel=All", + "browser.startup.homepage.abouthome_cache.testing=true", + "browser.startup.page=1", + "browser.newtabpage.activity-stream.discoverystream.endpoints=data:", + "browser.newtabpage.activity-stream.feeds.system.topstories=true", + "browser.newtabpage.activity-stream.feeds.section.topstories=true", + "browser.newtabpage.activity-stream.feeds.system.topstories=true", + "browser.newtabpage.activity-stream.feeds.section.topstories.options={\"provider_name\":\"\"}", + "browser.newtabpage.activity-stream.telemetry.structuredIngestion=false", + "browser.newtabpage.activity-stream.discoverystream.endpoints=https://example.com", + "dom.ipc.processPrelaunch.delayMs=0", + # Bug 1694957 is why we need dom.ipc.processPrelaunch.delayMs=0 +] + +["browser_basic_endtoend.js"] + +["browser_bump_version.js"] + +["browser_disabled.js"] + +["browser_experiments_api_control.js"] + +["browser_locale_change.js"] + +["browser_no_cache.js"] + +["browser_no_cache_on_SessionStartup_restore.js"] + +["browser_no_startup_actions.js"] + +["browser_overwrite_cache.js"] + +["browser_process_crash.js"] +skip-if = [ + "!crashreporter", + "os == 'mac' && fission", # Bug 1659427; medium frequency intermittent on osx: test timed out +] + +["browser_same_consumer.js"] + +["browser_sanitize.js"] + +["browser_shutdown_timeout.js"] diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_basic_endtoend.js b/browser/components/newtab/test/browser/abouthomecache/browser_basic_endtoend.js new file mode 100644 index 0000000000..bd42dd4af9 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_basic_endtoend.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the about:home cache gets written on shutdown, and read + * from in the subsequent startup. + */ +add_task(async function test_basic_behaviour() { + await withFullyLoadedAboutHome(async browser => { + // First, clear the cache to test the base case. + await clearCache(); + await simulateRestart(browser); + await ensureCachedAboutHome(browser); + + // Next, test that a subsequent restart also shows the cached + // about:home. + await simulateRestart(browser); + await ensureCachedAboutHome(browser); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_bump_version.js b/browser/components/newtab/test/browser/abouthomecache/browser_bump_version.js new file mode 100644 index 0000000000..726b9aa973 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_bump_version.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that if the "version" metadata on the cache entry doesn't match + * the expectation that we ignore the cache and load the dynamic about:home + * document. + */ +add_task(async function test_bump_version() { + await withFullyLoadedAboutHome(async browser => { + // First, ensure that a pre-existing cache exists. + await simulateRestart(browser); + + let cacheEntry = await AboutHomeStartupCache.ensureCacheEntry(); + Assert.equal( + cacheEntry.getMetaDataElement("version"), + Services.appinfo.appBuildID, + "Cache entry should be versioned on the build ID" + ); + cacheEntry.setMetaDataElement("version", "somethingnew"); + // We don't need to shutdown write or ensure the cache wins the race, + // since we expect the cache to be blown away because the version number + // has been bumped. + await simulateRestart(browser, { + withAutoShutdownWrite: false, + ensureCacheWinsRace: false, + }); + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.INVALIDATED + ); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_disabled.js b/browser/components/newtab/test/browser/abouthomecache/browser_disabled.js new file mode 100644 index 0000000000..faa79b219c --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_disabled.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This file tests scenarios where the cache is disabled due to user + * configuration. + */ + +registerCleanupFunction(async () => { + // When the test completes, make sure we cleanup with a populated cache, + // since this is the default starting state for these tests. + await withFullyLoadedAboutHome(async browser => { + await simulateRestart(browser); + }); +}); + +/** + * Tests the case where the cache is disabled via the pref. + */ +add_task(async function test_cache_disabled() { + await withFullyLoadedAboutHome(async browser => { + await SpecialPowers.pushPrefEnv({ + set: [["browser.startup.homepage.abouthome_cache.enabled", false]], + }); + + await simulateRestart(browser); + + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.DISABLED + ); + + await SpecialPowers.popPrefEnv(); + }); +}); + +/** + * Tests the case where the cache is disabled because the home page is + * not set at about:home. + */ +add_task(async function test_cache_custom_homepage() { + await withFullyLoadedAboutHome(async browser => { + await HomePage.set("https://example.com"); + await simulateRestart(browser); + + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.NOT_LOADING_ABOUTHOME + ); + + HomePage.reset(); + }); +}); + +/** + * Tests the case where the cache is disabled because the session is + * configured to automatically be restored. + */ +add_task(async function test_cache_restore_session() { + await withFullyLoadedAboutHome(async browser => { + await SpecialPowers.pushPrefEnv({ + set: [["browser.startup.page", 3]], + }); + + await simulateRestart(browser); + + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.NOT_LOADING_ABOUTHOME + ); + + await SpecialPowers.popPrefEnv(); + }); +}); + +/** + * Tests the case where the cache is disabled because about:newtab + * preloading is disabled. + */ +add_task(async function test_cache_no_preloading() { + await withFullyLoadedAboutHome(async browser => { + await SpecialPowers.pushPrefEnv({ + set: [["browser.newtab.preload", false]], + }); + + await simulateRestart(browser); + + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.PRELOADING_DISABLED + ); + + await SpecialPowers.popPrefEnv(); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_experiments_api_control.js b/browser/components/newtab/test/browser/abouthomecache/browser_experiments_api_control.js new file mode 100644 index 0000000000..a94f1fe055 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_experiments_api_control.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ExperimentFakes } = ChromeUtils.importESModule( + "resource://testing-common/NimbusTestUtils.sys.mjs" +); + +registerCleanupFunction(async () => { + // When the test completes, make sure we cleanup with a populated cache, + // since this is the default starting state for these tests. + await withFullyLoadedAboutHome(async browser => { + await simulateRestart(browser); + }); +}); + +/** + * Tests that the ExperimentsAPI mechanism can be used to remotely + * enable and disable the about:home startup cache. + */ +add_task(async function test_experiments_api_control() { + // First, the disabled case. + await withFullyLoadedAboutHome(async browser => { + let doEnrollmentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "abouthomecache", + value: { enabled: false }, + }); + + Assert.ok( + !NimbusFeatures.abouthomecache.getVariable("enabled"), + "NimbusFeatures should tell us that the about:home startup cache " + + "is disabled" + ); + + await simulateRestart(browser); + + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.DISABLED + ); + + await doEnrollmentCleanup(); + }); + + // Now the enabled case. + await withFullyLoadedAboutHome(async browser => { + let doEnrollmentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "abouthomecache", + value: { enabled: true }, + }); + + Assert.ok( + NimbusFeatures.abouthomecache.getVariable("enabled"), + "NimbusFeatures should tell us that the about:home startup cache " + + "is enabled" + ); + + await simulateRestart(browser); + await ensureCachedAboutHome(browser); + await doEnrollmentCleanup(); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_locale_change.js b/browser/components/newtab/test/browser/abouthomecache/browser_locale_change.js new file mode 100644 index 0000000000..e9e3c619ec --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_locale_change.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the about:home startup cache is cleared if the app + * locale changes. + */ +add_task(async function test_locale_change() { + await withFullyLoadedAboutHome(async browser => { + await simulateRestart(browser); + await ensureCachedAboutHome(browser); + + Services.obs.notifyObservers(null, "intl:app-locales-changed"); + await AboutHomeStartupCache.ensureCacheEntry(); + + // We're testing that switching locales blows away the cache, so we + // bypass the automatic writing of the cache on shutdown, and we + // also don't need to wait for the cache to be available. + await simulateRestart(browser, { + withAutoShutdownWrite: false, + ensureCacheWinsRace: false, + }); + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.DOES_NOT_EXIST + ); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_no_cache.js b/browser/components/newtab/test/browser/abouthomecache/browser_no_cache.js new file mode 100644 index 0000000000..fdb51f8712 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_no_cache.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +/** + * Test that if there's no cache written, that we load the dynamic + * about:home document on startup. + */ +add_task(async function test_no_cache() { + await withFullyLoadedAboutHome(async browser => { + await clearCache(); + // We're testing the no-cache case, so we bypass the automatic writing + // of the cache on shutdown, and we also don't need to wait for the + // cache to be available. + await simulateRestart(browser, { + withAutoShutdownWrite: false, + ensureCacheWinsRace: false, + }); + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.DOES_NOT_EXIST + ); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_no_cache_on_SessionStartup_restore.js b/browser/components/newtab/test/browser/abouthomecache/browser_no_cache_on_SessionStartup_restore.js new file mode 100644 index 0000000000..a312b2b44f --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_no_cache_on_SessionStartup_restore.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if somehow about:newtab loads before about:home does, that we + * don't use the cache. This is because about:newtab doesn't use the cache, + * and so it'll inevitably be newer than what's in the about:home cache, + * which will put the about:home cache out of date the next time about:home + * eventually loads. + */ +add_task(async function test_no_cache_on_SessionStartup_restore() { + await withFullyLoadedAboutHome(async browser => { + await simulateRestart(browser, { skipAboutHomeLoad: true }); + + // We remove the preloaded browser to ensure that loading the next + // about:newtab occurs now, and not at preloading time. + NewTabPagePreloading.removePreloadedBrowser(window); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:newtab" + ); + + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + + // The cache is disqualified because about:newtab was loaded first. + // So now it's too late to use the cache. + await ensureDynamicAboutHome( + newWin.gBrowser.selectedBrowser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.LATE + ); + + await BrowserTestUtils.closeWindow(newWin); + await BrowserTestUtils.removeTab(tab); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_no_startup_actions.js b/browser/components/newtab/test/browser/abouthomecache/browser_no_startup_actions.js new file mode 100644 index 0000000000..255b4c9d21 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_no_startup_actions.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that upon initializing Activity Stream, the cached about:home + * document does not process any actions caused by that initialization. + * This is because the restored Redux state from the cache should be enough, + * and processing any of the initialization messages from Activity Stream + * could wipe out that state and cause flicker / unnecessary redraws. + */ +add_task(async function test_no_startup_actions() { + await withFullyLoadedAboutHome(async browser => { + // Make sure we have a cached document. We simulate a restart to ensure + // that we start with a cache... that we can then clear without a problem, + // before writing a new cache. This ensures that no matter what, we're in a + // state where we have a fresh cache, regardless of what's happened in earlier + // tests. + await simulateRestart(browser); + await clearCache(); + await simulateRestart(browser); + await ensureCachedAboutHome(browser); + + // Set up a listener to monitor for actions that get dispatched in the + // browser when we fire Activity Stream up again. + await SpecialPowers.spawn(browser, [], async () => { + let xrayWindow = ChromeUtils.waiveXrays(content); + xrayWindow.nonStartupActions = []; + xrayWindow.startupActions = []; + xrayWindow.RPMAddMessageListener("ActivityStream:MainToContent", msg => { + if (msg.data.meta.isStartup) { + xrayWindow.startupActions.push(msg.data); + } else { + xrayWindow.nonStartupActions.push(msg.data); + } + }); + }); + + // The following two statements seem to be enough to simulate Activity + // Stream starting up. + AboutNewTab.activityStream.uninit(); + AboutNewTab.onBrowserReady(); + + // Much of Activity Stream initializes asynchronously. This is the easiest way + // I could find to ensure that enough of the feeds had initialized to produce + // a meaningful cached document. + await TestUtils.waitForCondition(() => { + let feed = AboutNewTab.activityStream.store.feeds.get( + "feeds.discoverystreamfeed" + ); + return feed?.loaded; + }); + + // Wait an additional few seconds for any other actions to get displayed. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 2000)); + + let [startupActions, nonStartupActions] = await SpecialPowers.spawn( + browser, + [], + async () => { + let xrayWindow = ChromeUtils.waiveXrays(content); + return [xrayWindow.startupActions, xrayWindow.nonStartupActions]; + } + ); + + Assert.ok(!!startupActions.length, "Should have seen startup actions."); + info(`Saw ${startupActions.length} startup actions.`); + + Assert.equal( + nonStartupActions.length, + 0, + "Should be no non-startup actions." + ); + + if (nonStartupActions.length) { + for (let action of nonStartupActions) { + info(`Non-startup action: ${action.type}`); + } + } + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_overwrite_cache.js b/browser/components/newtab/test/browser/abouthomecache/browser_overwrite_cache.js new file mode 100644 index 0000000000..22df98794f --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_overwrite_cache.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if a pre-existing about:home cache exists, that it can + * be overwritten with new information. + */ +add_task(async function test_overwrite_cache() { + await withFullyLoadedAboutHome(async browser => { + await simulateRestart(browser); + const TEST_ID = "test_overwrite_cache_h1"; + + // We need the CSP meta tag in about: pages, otherwise we hit assertions in + // debug builds. + await injectIntoCache( + ` + <html> + <head> + <meta http-equiv="Content-Security-Policy" content="default-src 'none'; object-src 'none'; script-src resource: chrome:; connect-src https:; img-src https: data: blob:; style-src 'unsafe-inline';"> + </head> + <body> + <h1 id="${TEST_ID}">Something new</h1> + <div id="root"></div> + </body> + <script src="about:home?jscache"></script> + </html>`, + "window.__FROM_STARTUP_CACHE__ = true;" + ); + await simulateRestart(browser, { withAutoShutdownWrite: false }); + + await SpecialPowers.spawn(browser, [TEST_ID], async testID => { + let target = content.document.getElementById(testID); + Assert.ok(target, "Found the target element"); + }); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_process_crash.js b/browser/components/newtab/test/browser/abouthomecache/browser_process_crash.js new file mode 100644 index 0000000000..d3bfa383c2 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_process_crash.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that if the "privileged about content process" crashes, that it + * drops its internal reference to the "privileged about content process" + * process manager, and that a subsequent restart of that process type + * results in a dynamic document load. Also tests that crashing of + * any other content process type doesn't clear the process manager + * reference. + */ +add_task(async function test_process_crash() { + await withFullyLoadedAboutHome(async browser => { + await simulateRestart(browser); + let origProcManager = AboutHomeStartupCache._procManager; + + await BrowserTestUtils.crashFrame(browser); + Assert.notEqual( + origProcManager, + AboutHomeStartupCache._procManager, + "Should have dropped the reference to the crashed process" + ); + }); + + await withFullyLoadedAboutHome(async browser => { + // The cache should still be considered "valid and used", since it was + // used successfully before the crash. + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.VALID_AND_USED + ); + + // Now simulate a restart to attach the AboutHomeStartupCache to + // the new privileged about content process. + await simulateRestart(browser); + }); + + let latestProcManager = AboutHomeStartupCache._procManager; + + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await BrowserTestUtils.withNewTab("http://example.com", async browser => { + await BrowserTestUtils.crashFrame(browser); + Assert.equal( + latestProcManager, + AboutHomeStartupCache._procManager, + "Should still have the reference to the privileged about process" + ); + }); +}); + +/** + * Tests that if the "privileged about content process" crashes while + * a cache request is still underway, that the cache request resolves with + * null input streams. + */ +add_task(async function test_process_crash_while_requesting_streams() { + await withFullyLoadedAboutHome(async browser => { + await simulateRestart(browser); + let cacheStreamsPromise = AboutHomeStartupCache.requestCache(); + await BrowserTestUtils.crashFrame(browser); + let cacheStreams = await cacheStreamsPromise; + + if (!cacheStreams.pageInputStream && !cacheStreams.scriptInputStream) { + Assert.ok(true, "Page and script input streams are null."); + } else { + // It's possible (but probably rare) the parent was able to receive the + // streams before the crash occurred. In that case, we'll make sure that + // we can still read the streams. + info("Received the streams. Checking that they're readable."); + Assert.ok( + cacheStreams.pageInputStream.available(), + "Bytes available for page stream" + ); + Assert.ok( + cacheStreams.scriptInputStream.available(), + "Bytes available for script stream" + ); + } + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_same_consumer.js b/browser/components/newtab/test/browser/abouthomecache/browser_same_consumer.js new file mode 100644 index 0000000000..75f8875f26 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_same_consumer.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if a page attempts to load the script stream without + * having also loaded the page stream, that it will fail and get + * the default non-cached script. + */ +add_task(async function test_same_consumer() { + await withFullyLoadedAboutHome(async browser => { + await simulateRestart(browser); + + // We need the CSP meta tag in about: pages, otherwise we hit assertions in + // debug builds. + // + // We inject a script that sets a __CACHE_CONSUMED__ property to true on + // the window element. We'll test to ensure that if we try to load the + // script cache from a different BrowsingContext that this property is + // not set. + await injectIntoCache( + ` + <html> + <head> + <meta http-equiv="Content-Security-Policy" content="default-src 'none'; object-src 'none'; script-src resource: chrome:; connect-src https:; img-src https: data: blob:; style-src 'unsafe-inline';"> + </head> + <body> + <h1>A fake about:home page</h1> + <div id="root"></div> + </body> + </html>`, + "window.__CACHE_CONSUMED__ = true;" + ); + await simulateRestart(browser, { withAutoShutdownWrite: false }); + + // Attempting to load the script from the cache should fail, and instead load + // the markup. + await BrowserTestUtils.withNewTab("about:home?jscache", async browser2 => { + await SpecialPowers.spawn(browser2, [], async () => { + Assert.ok( + !Cu.waiveXrays(content).__CACHE_CONSUMED__, + "Should not have found __CACHE_CONSUMED__ property" + ); + Assert.ok( + content.document.body.classList.contains("activity-stream"), + "Should have found activity-stream class on <body> element" + ); + }); + }); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_sanitize.js b/browser/components/newtab/test/browser/abouthomecache/browser_sanitize.js new file mode 100644 index 0000000000..4dc7ba2c89 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_sanitize.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that when sanitizing places history, session store or downloads, that + * the about:home cache gets blown away. + */ + +add_task(async function test_sanitize() { + let testFlags = [ + ["downloads", Ci.nsIClearDataService.CLEAR_DOWNLOADS], + ["places history", Ci.nsIClearDataService.CLEAR_HISTORY], + ["session history", Ci.nsIClearDataService.CLEAR_SESSION_HISTORY], + ]; + + await withFullyLoadedAboutHome(async browser => { + for (let [type, flag] of testFlags) { + await simulateRestart(browser); + await ensureCachedAboutHome(browser); + + info( + "Testing that the about:home startup cache is cleared when " + + `clearing ${type}` + ); + + await new Promise((resolve, reject) => { + Services.clearData.deleteData(flag, { + onDataDeleted(resultFlags) { + if (!resultFlags) { + resolve(); + } else { + reject(new Error(`Failed with flags: ${resultFlags}`)); + } + }, + }); + }); + + // For the purposes of the test, we don't want the write-on-shutdown + // behaviour here (because we just want to test that the cache doesn't + // exist on startup if the history data was cleared). We also therefore + // don't need to ensure that the cache wins the race. + await simulateRestart(browser, { + withAutoShutdownWrite: false, + ensureCacheWinsRace: false, + }); + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.DOES_NOT_EXIST + ); + } + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_shutdown_timeout.js b/browser/components/newtab/test/browser/abouthomecache/browser_shutdown_timeout.js new file mode 100644 index 0000000000..b1600bfe00 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_shutdown_timeout.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if there's a substantial delay in getting the cache + * streams from the privileged about content process for any reason + * during shutdown, that we timeout and let the AsyncShutdown proceed, + * rather than letting it block until AsyncShutdown causes a shutdown + * hang crash. + */ +add_task(async function test_shutdown_timeout() { + await withFullyLoadedAboutHome(async browser => { + // First, make sure the cache is populated so that later on, after + // the timeout, simulateRestart doesn't complain about not finding + // a pre-existing cache. This complaining only happens if this test + // is run in isolation. + await clearCache(); + await simulateRestart(browser); + + // Next, manually shutdown the AboutHomeStartupCacheChild so that + // it doesn't respond to requests to the cache streams. + await SpecialPowers.spawn(browser, [], async () => { + let { AboutHomeStartupCacheChild } = ChromeUtils.importESModule( + "resource:///modules/AboutNewTabService.sys.mjs" + ); + AboutHomeStartupCacheChild.uninit(); + }); + + // Then, manually dirty the cache state so that we attempt to write + // on shutdown. + AboutHomeStartupCache.onPreloadedNewTabMessage(); + + await simulateRestart(browser, { expectTimeout: true }); + + Assert.ok( + true, + "We reached here, which means shutdown didn't block forever." + ); + + // Clear the cache so that we're not in a half-persisted state. + await clearCache(); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/head.js b/browser/components/newtab/test/browser/abouthomecache/head.js new file mode 100644 index 0000000000..5599b2bd10 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/head.js @@ -0,0 +1,365 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let { AboutHomeStartupCache } = ChromeUtils.importESModule( + "resource:///modules/BrowserGlue.sys.mjs" +); +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); +const { DiscoveryStreamFeed } = ChromeUtils.importESModule( + "resource://activity-stream/lib/DiscoveryStreamFeed.sys.mjs" +); + +// Some Activity Stream preferences are JSON encoded, and quite complex. +// Hard-coding them here or in browser.ini makes them brittle to change. +// Instead, we pull the default prefs structures and set the values that +// we need and write them to preferences here dynamically. We do this in +// its own scope to avoid polluting the global scope. +{ + const { PREFS_CONFIG } = ChromeUtils.importESModule( + "resource://activity-stream/lib/ActivityStream.sys.mjs" + ); + + let defaultDSConfig = JSON.parse( + PREFS_CONFIG.get("discoverystream.config").getValue({ + geo: "US", + locale: "en-US", + }) + ); + + // Configure Activity Stream to query for the layout JSON file that points + // at the local top stories feed. + Services.prefs.setCharPref( + "browser.newtabpage.activity-stream.discoverystream.config", + JSON.stringify(defaultDSConfig) + ); +} + +/** + * Utility function that loads about:home in the current window in a new tab, and waits + * for the Discovery Stream cards to finish loading before running the taskFn function. + * Once taskFn exits, the about:home tab will be closed. + * + * @param {function} taskFn + * A function that will be run after about:home has finished loading. This can be + * an async function. + * @return {Promise} + * @resolves {undefined} + */ +function withFullyLoadedAboutHome(taskFn) { + const sandbox = sinon.createSandbox(); + sandbox + .stub(DiscoveryStreamFeed.prototype, "generateFeedUrl") + .returns( + "https://example.com/browser/browser/components/newtab/test/browser/topstories.json" + ); + + return BrowserTestUtils.withNewTab("about:home", async browser => { + await SpecialPowers.spawn(browser, [], async () => { + await ContentTaskUtils.waitForCondition( + () => + content.document.querySelectorAll( + "[data-section-id='topstories'] .ds-card-link" + ).length, + "Waiting for Discovery Stream to be rendered." + ); + }); + + await taskFn(browser); + sandbox.restore(); + }); +} + +/** + * Shuts down the AboutHomeStartupCache components in the parent process + * and privileged about content process, and then restarts them, simulating + * the parent process having restarted. + * + * @param browser (<xul:browser>) + * A <xul:browser> with about:home running in it. This will be reloaded + * after the restart simultion is complete, and that reload will attempt + * to read any about:home cache contents. + * @param options (object, optional) + * + * An object with the following properties: + * + * withAutoShutdownWrite (boolean, optional): + * Whether or not the shutdown part of the simulation should cause the + * shutdown handler to run, which normally causes the cache to be + * written. Setting this to false is handy if the cache has been + * specially prepared for the subsequent startup, and we don't want to + * overwrite it. This defaults to true. + * + * ensureCacheWinsRace (boolean, optional): + * Ensures that the privileged about content process will be able to + * read the bytes from the streams sent down from the HTTP cache. Use + * this to avoid the HTTP cache "losing the race" against reading the + * about:home document from the omni.ja. This defaults to true. + * + * expectTimeout (boolean, optional): + * If true, indicates that it's expected that AboutHomeStartupCache will + * timeout when shutting down. If false, such timeouts will result in + * test failures. Defaults to false. + * + * skipAboutHomeLoad (boolean, optional): + * If true, doesn't automatically load about:home after the simulated + * restart. Defaults to false. + * + * @returns Promise + * @resolves undefined + * Resolves once the restart simulation is complete, and the <xul:browser> + * pointed at about:home finishes reloading. + */ +async function simulateRestart( + browser, + { + withAutoShutdownWrite = true, + ensureCacheWinsRace = true, + expectTimeout = false, + skipAboutHomeLoad = false, + } = {} +) { + info("Simulating restart of the browser"); + if (browser.remoteType !== E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE) { + throw new Error( + "prepareLoadFromCache should only be called on a browser " + + "loaded in the privileged about content process." + ); + } + + if (withAutoShutdownWrite && AboutHomeStartupCache.initted) { + info("Simulating shutdown write"); + let timedOut = !(await AboutHomeStartupCache.onShutdown(expectTimeout)); + if (timedOut && !expectTimeout) { + Assert.ok( + false, + "AboutHomeStartupCache shutdown unexpectedly timed out." + ); + } else if (!timedOut && expectTimeout) { + Assert.ok(false, "AboutHomeStartupCache shutdown failed to time out."); + } + info("Shutdown write done"); + } else { + info("Intentionally skipping shutdown write"); + } + + AboutHomeStartupCache.uninit(); + + info("Waiting for AboutHomeStartupCacheChild to uninit"); + await SpecialPowers.spawn(browser, [], async () => { + let { AboutHomeStartupCacheChild } = ChromeUtils.importESModule( + "resource:///modules/AboutNewTabService.sys.mjs" + ); + AboutHomeStartupCacheChild.uninit(); + }); + info("AboutHomeStartupCacheChild uninitted"); + + AboutHomeStartupCache.init(); + + if (AboutHomeStartupCache.initted) { + let processManager = browser.messageManager.processMessageManager; + let pp = browser.browsingContext.currentWindowGlobal.domProcess; + let { childID } = pp; + AboutHomeStartupCache.onContentProcessCreated(childID, processManager, pp); + + info("Waiting for AboutHomeStartupCache cache entry"); + await AboutHomeStartupCache.ensureCacheEntry(); + info("Got AboutHomeStartupCache cache entry"); + + if (ensureCacheWinsRace) { + info("Ensuring cache bytes are available"); + await SpecialPowers.spawn(browser, [], async () => { + let { AboutHomeStartupCacheChild } = ChromeUtils.importESModule( + "resource:///modules/AboutNewTabService.sys.mjs" + ); + let pageStream = AboutHomeStartupCacheChild._pageInputStream; + let scriptStream = AboutHomeStartupCacheChild._scriptInputStream; + await ContentTaskUtils.waitForCondition(() => { + return pageStream.available() && scriptStream.available(); + }); + }); + } + } + + if (!skipAboutHomeLoad) { + info("Waiting for about:home to load"); + let loaded = BrowserTestUtils.browserLoaded(browser, false, "about:home"); + BrowserTestUtils.startLoadingURIString(browser, "about:home"); + await loaded; + info("about:home loaded"); + } +} + +/** + * Writes a page string and a script string into the cache for + * the next about:home load. + * + * @param page (String) + * The HTML content to write into the cache. This cannot be the empty + * string. Note that this string should contain a node that has an + * id of "root", in order for the newtab scripts to attach correctly. + * Otherwise, an exception might get thrown which can cause shutdown + * leaks. + * @param script (String) + * The JS content to write into the cache that can be loaded via + * about:home?jscache. This cannot be the empty string. + * @returns Promise + * @resolves undefined + * When the page and script content has been successfully written. + */ +async function injectIntoCache(page, script) { + if (!page || !script) { + throw new Error("Cannot injectIntoCache with falsey values"); + } + + if (!page.includes(`id="root"`)) { + throw new Error("Page markup must include a root node."); + } + + await AboutHomeStartupCache.ensureCacheEntry(); + + let pageInputStream = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + + pageInputStream.setUTF8Data(page); + + let scriptInputStream = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + + scriptInputStream.setUTF8Data(script); + + await AboutHomeStartupCache.populateCache(pageInputStream, scriptInputStream); +} + +/** + * Clears out any pre-existing about:home cache. + * @returns Promise + * @resolves undefined + * Resolves when the cache is cleared. + */ +async function clearCache() { + info("Test is clearing the cache"); + AboutHomeStartupCache.clearCache(); + await AboutHomeStartupCache.ensureCacheEntry(); + info("Test has cleared the cache."); +} + +/** + * Checks that the browser.startup.abouthome_cache_result scalar was + * recorded at a particular value. + * + * @param cacheResultScalar (Number) + * One of the AboutHomeStartupCache.CACHE_RESULT_SCALARS values. + */ +function assertCacheResultScalar(cacheResultScalar) { + let parentScalars = Services.telemetry.getSnapshotForScalars("main").parent; + Assert.equal( + parentScalars["browser.startup.abouthome_cache_result"], + cacheResultScalar, + "Expected the right value set to browser.startup.abouthome_cache_result " + + "scalar." + ); +} + +/** + * Tests that the about:home document loaded in a passed <xul:browser> was + * one from the cache. + * + * We test for this by looking for some tell-tale signs of the cached + * document: + * + * 1. The about:home?jscache <script> element + * 2. The __FROM_STARTUP_CACHE__ expando on the window + * 3. The "activity-stream" class on the document body + * 4. The top sites section + * + * @param browser (<xul:browser>) + * A <xul:browser> with about:home running in it. + * @returns Promise + * @resolves undefined + * Resolves once the cache entry has been destroyed. + */ +async function ensureCachedAboutHome(browser) { + await SpecialPowers.spawn(browser, [], async () => { + let syncScripts = Array.from( + content.document.querySelectorAll("script:not([type='module'])") + ); + Assert.ok(!!syncScripts.length, "There should be page scripts."); + let [lastSyncScript] = syncScripts.reverse(); + Assert.equal( + lastSyncScript.src, + "about:home?jscache", + "Found about:home?jscache script tag, indicating the cached doc" + ); + Assert.ok( + Cu.waiveXrays(content).__FROM_STARTUP_CACHE__, + "Should have found window.__FROM_STARTUP_CACHE__" + ); + Assert.ok( + content.document.body.classList.contains("activity-stream"), + "Should have found activity-stream class on <body> element" + ); + Assert.ok( + content.document.querySelector("[data-section-id='topsites']"), + "Should have found the Discovery Stream top sites." + ); + }); + assertCacheResultScalar( + AboutHomeStartupCache.CACHE_RESULT_SCALARS.VALID_AND_USED + ); +} + +/** + * Tests that the about:home document loaded in a passed <xul:browser> was + * dynamically generated, and _not_ from the cache. + * + * We test for this by looking for some tell-tale signs of the dynamically + * generated document: + * + * 1. No <script> elements (the scripts are loaded from the ScriptPreloader + * via AboutNewTabChild when the "privileged about content process" is + * enabled) + * 2. No __FROM_STARTUP_CACHE__ expando on the window + * 3. The "activity-stream" class on the document body + * 4. The top sites section + * + * @param browser (<xul:browser>) + * A <xul:browser> with about:home running in it. + * @param expectedResultScalar (Number) + * One of the AboutHomeStartupCache.CACHE_RESULT_SCALARS values. It is + * asserted that the cache result Telemetry scalar will have been set + * to this value to explain why the dynamic about:home was used. + * @returns Promise + * @resolves undefined + * Resolves once the cache entry has been destroyed. + */ +async function ensureDynamicAboutHome(browser, expectedResultScalar) { + await SpecialPowers.spawn(browser, [], async () => { + let syncScripts = Array.from( + content.document.querySelectorAll("script:not([type='module'])") + ); + Assert.equal(syncScripts.length, 0, "There should be no page scripts."); + + Assert.equal( + Cu.waiveXrays(content).__FROM_STARTUP_CACHE__, + undefined, + "Should not have found window.__FROM_STARTUP_CACHE__" + ); + + Assert.ok( + content.document.body.classList.contains("activity-stream"), + "Should have found activity-stream class on <body> element" + ); + Assert.ok( + content.document.querySelector("[data-section-id='topsites']"), + "Should have found the Discovery Stream top sites." + ); + }); + + assertCacheResultScalar(expectedResultScalar); +} diff --git a/browser/components/newtab/test/browser/annotation_first.html b/browser/components/newtab/test/browser/annotation_first.html new file mode 100644 index 0000000000..e40ed1db6c --- /dev/null +++ b/browser/components/newtab/test/browser/annotation_first.html @@ -0,0 +1,2 @@ +first +<a href="annotation_second.html">goto second</a> diff --git a/browser/components/newtab/test/browser/annotation_second.html b/browser/components/newtab/test/browser/annotation_second.html new file mode 100644 index 0000000000..8d8bbab6bd --- /dev/null +++ b/browser/components/newtab/test/browser/annotation_second.html @@ -0,0 +1,2 @@ +second +<a href="https://www.example.com/browser/browser/components/newtab/test/browser/annotation_third.html">goto third</a> diff --git a/browser/components/newtab/test/browser/annotation_third.html b/browser/components/newtab/test/browser/annotation_third.html new file mode 100644 index 0000000000..b63f85fe1f --- /dev/null +++ b/browser/components/newtab/test/browser/annotation_third.html @@ -0,0 +1,2 @@ +thrid +<a href="https://example.org/">goto outside</a> diff --git a/browser/components/newtab/test/browser/blue_page.html b/browser/components/newtab/test/browser/blue_page.html new file mode 100644 index 0000000000..e7eaba1e1c --- /dev/null +++ b/browser/components/newtab/test/browser/blue_page.html @@ -0,0 +1,6 @@ +<html> + <head> + <meta charset="utf-8"> + </head> + <body style="background-color: blue" /> +</html> diff --git a/browser/components/newtab/test/browser/browser.toml b/browser/components/newtab/test/browser/browser.toml new file mode 100644 index 0000000000..f9c9611c2e --- /dev/null +++ b/browser/components/newtab/test/browser/browser.toml @@ -0,0 +1,81 @@ +[DEFAULT] +support-files = [ + "blue_page.html", + "red_page.html", + "annotation_first.html", + "annotation_second.html", + "annotation_third.html", + "head.js", + "redirect_to.sjs", + "topstories.json", + "file_pdf.PDF", +] +prefs = [ + "browser.newtabpage.activity-stream.debug=false", + "browser.newtabpage.activity-stream.discoverystream.enabled=true", + "browser.newtabpage.activity-stream.discoverystream.endpoints=data:", + "browser.newtabpage.activity-stream.feeds.system.topstories=true", + "browser.newtabpage.activity-stream.feeds.section.topstories=true", + "browser.newtabpage.activity-stream.feeds.section.topstories.options={\"provider_name\":\"\"}", + "messaging-system.log=all", +] + +["browser_as_load_location.js"] + +["browser_as_render.js"] + +["browser_context_menu_item.js"] + +["browser_customize_menu_content.js"] +skip-if = ["os == 'linux' && tsan"] #Bug 1687896 +https_first_disabled = true + +["browser_customize_menu_render.js"] + +["browser_discovery_card.js"] + +["browser_discovery_render.js"] + +["browser_enabled_newtabpage.js"] + +["browser_foxdoodle_set_default.js"] + +["browser_getScreenshots.js"] + +["browser_highlights_section.js"] + +["browser_multistage_spotlight.js"] + +["browser_multistage_spotlight_telemetry.js"] +skip-if = ["verify"] # bug 1834620 - order of events not stable + +["browser_newtab_glean.js"] + +["browser_newtab_header.js"] + +["browser_newtab_last_LinkMenu.js"] + +["browser_newtab_overrides.js"] + +["browser_newtab_ping.js"] + +["browser_newtab_towindow.js"] + +["browser_newtab_trigger.js"] + +["browser_open_tab_focus.js"] +skip-if = ["os == 'linux'"] # Test setup only implemented for OSX and Windows + +["browser_remote_l10n.js"] + +["browser_topsites_annotation.js"] +skip-if = [ + "os == 'linux' && debug", # Bug 1785005 + "os == 'linux' && asan", # Bug 1785005 +] + +["browser_topsites_contextMenu_options.js"] + +["browser_topsites_section.js"] + +["browser_trigger_messagesLoaded.js"] diff --git a/browser/components/newtab/test/browser/browser_as_load_location.js b/browser/components/newtab/test/browser/browser_as_load_location.js new file mode 100644 index 0000000000..f11b6cf503 --- /dev/null +++ b/browser/components/newtab/test/browser/browser_as_load_location.js @@ -0,0 +1,44 @@ +"use strict"; + +/** + * Helper to test that a newtab page loads its html document. + * + * @param selector {String} CSS selector to find an element in newtab content + * @param message {String} Description of the test printed with the assertion + */ +async function checkNewtabLoads(selector, message) { + // simulate a newtab open as a user would + BrowserOpenTab(); + + // wait until the browser loads + let browser = gBrowser.selectedBrowser; + await waitForPreloaded(browser); + + // check what the content task thinks has been loaded. + let found = await ContentTask.spawn( + browser, + selector, + arg => content.document.querySelector(arg) !== null + ); + ok(found, message); + + // avoid leakage + BrowserTestUtils.removeTab(gBrowser.selectedTab); +} + +// Test with activity stream on +async function checkActivityStreamLoads() { + await checkNewtabLoads( + "body.activity-stream", + "Got <body class='activity-stream'> Element" + ); +} + +// Run a first time not from a preloaded browser +add_task(async function checkActivityStreamNotPreloadedLoad() { + NewTabPagePreloading.removePreloadedBrowser(window); + await checkActivityStreamLoads(); +}); + +// Run a second time from a preloaded browser +add_task(checkActivityStreamLoads); diff --git a/browser/components/newtab/test/browser/browser_as_render.js b/browser/components/newtab/test/browser/browser_as_render.js new file mode 100644 index 0000000000..2e82786b16 --- /dev/null +++ b/browser/components/newtab/test/browser/browser_as_render.js @@ -0,0 +1,83 @@ +"use strict"; + +test_newtab({ + async before({ pushPrefs }) { + await pushPrefs([ + "browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", + false, + ]); + }, + test: function test_render_search() { + let search = content.document.getElementById("newtab-search-text"); + ok(search, "Got the search box"); + isnot( + search.placeholder, + "search_web_placeholder", + "Search box is localized" + ); + }, +}); + +test_newtab({ + async before({ pushPrefs }) { + await pushPrefs([ + "browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", + true, + ]); + }, + test: function test_render_search_handoff() { + let search = content.document.querySelector(".search-handoff-button"); + ok(search, "Got the search handoff button"); + }, +}); + +test_newtab(function test_render_topsites() { + let topSites = content.document.querySelector(".top-sites-list"); + ok(topSites, "Got the top sites section"); +}); + +test_newtab({ + async before({ pushPrefs }) { + await pushPrefs([ + "browser.newtabpage.activity-stream.feeds.topsites", + false, + ]); + }, + test: function test_render_no_topsites() { + let topSites = content.document.querySelector(".top-sites-list"); + ok(!topSites, "No top sites section"); + }, +}); + +// This next test runs immediately after test_render_no_topsites to make sure +// the topsites pref is restored +test_newtab(function test_render_topsites_again() { + let topSites = content.document.querySelector(".top-sites-list"); + ok(topSites, "Got the top sites section again"); +}); + +test_newtab({ + async before({ pushPrefs }) { + await pushPrefs([ + "browser.newtabpage.activity-stream.logowordmark.alwaysVisible", + false, + ]); + }, + test: function test_render_logo_false() { + let logoWordmark = content.document.querySelector(".logo-and-wordmark"); + ok(!logoWordmark, "The logo is not rendered when pref is false"); + }, +}); + +test_newtab({ + async before({ pushPrefs }) { + await pushPrefs([ + "browser.newtabpage.activity-stream.logowordmark.alwaysVisible", + true, + ]); + }, + test: function test_render_logo() { + let logoWordmark = content.document.querySelector(".logo-and-wordmark"); + ok(logoWordmark, "The logo is rendered when pref is true"); + }, +}); diff --git a/browser/components/newtab/test/browser/browser_context_menu_item.js b/browser/components/newtab/test/browser/browser_context_menu_item.js new file mode 100644 index 0000000000..6a4883ab93 --- /dev/null +++ b/browser/components/newtab/test/browser/browser_context_menu_item.js @@ -0,0 +1,18 @@ +"use strict"; + +// Test that we do not set icons in individual tile and card context menus on +// newtab page. +test_newtab({ + test: async function test_contextMenuIcons() { + const siteSelector = ".top-sites-list:not(.search-shortcut, .placeholder)"; + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(siteSelector), + "Topsites have loaded" + ); + const contextMenuItems = await content.openContextMenuAndGetOptions( + siteSelector + ); + let icon = contextMenuItems[0].querySelector(".icon"); + ok(!icon, "icon was not rendered"); + }, +}); diff --git a/browser/components/newtab/test/browser/browser_customize_menu_content.js b/browser/components/newtab/test/browser/browser_customize_menu_content.js new file mode 100644 index 0000000000..ba83f1ff0a --- /dev/null +++ b/browser/components/newtab/test/browser/browser_customize_menu_content.js @@ -0,0 +1,219 @@ +"use strict"; + +test_newtab({ + async before({ pushPrefs }) { + await pushPrefs( + ["browser.newtabpage.activity-stream.feeds.topsites", false], + ["browser.newtabpage.activity-stream.feeds.section.topstories", false], + ["browser.newtabpage.activity-stream.feeds.section.highlights", false] + ); + }, + test: async function test_render_customizeMenu() { + function getSection(sectionIdentifier) { + return content.document.querySelector( + `section[data-section-id="${sectionIdentifier}"]` + ); + } + function promiseSectionShown(sectionIdentifier) { + return ContentTaskUtils.waitForMutationCondition( + content.document.querySelector("main"), + { childList: true, subtree: true }, + () => getSection(sectionIdentifier) + ); + } + const TOPSITES_PREF = "browser.newtabpage.activity-stream.feeds.topsites"; + const HIGHLIGHTS_PREF = + "browser.newtabpage.activity-stream.feeds.section.highlights"; + const TOPSTORIES_PREF = + "browser.newtabpage.activity-stream.feeds.section.topstories"; + + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(".personalize-button"), + "Wait for prefs button to load on the newtab page" + ); + + let customizeButton = content.document.querySelector(".personalize-button"); + customizeButton.click(); + + let defaultPos = "matrix(1, 0, 0, 1, 0, 0)"; + await ContentTaskUtils.waitForCondition( + () => + content.getComputedStyle( + content.document.querySelector(".customize-menu") + ).transform === defaultPos, + "Customize Menu should be visible on screen" + ); + + // Test that clicking the shortcuts toggle will make the section + // appear on the newtab page. + // + // We waive XRay wrappers because we want to call the click() + // method defined on the toggle from this context. + let shortcutsSwitch = Cu.waiveXrays( + content.document.querySelector("#shortcuts-section moz-toggle") + ); + Assert.ok( + !Services.prefs.getBoolPref(TOPSITES_PREF), + "Topsites are turned off" + ); + Assert.ok(!getSection("topsites"), "Shortcuts section is not rendered"); + + let sectionShownPromise = promiseSectionShown("topsites"); + shortcutsSwitch.click(); + await sectionShownPromise; + + Assert.ok(getSection("topsites"), "Shortcuts section is rendered"); + + // Test that clicking the pocket toggle will make the pocket section + // appear on the newtab page + // + // We waive XRay wrappers because we want to call the click() + // method defined on the toggle from this context. + let pocketSwitch = Cu.waiveXrays( + content.document.querySelector("#pocket-section moz-toggle") + ); + Assert.ok( + !Services.prefs.getBoolPref(TOPSTORIES_PREF), + "Pocket pref is turned off" + ); + Assert.ok(!getSection("topstories"), "Pocket section is not rendered"); + + sectionShownPromise = promiseSectionShown("topstories"); + pocketSwitch.click(); + await sectionShownPromise; + + Assert.ok(getSection("topstories"), "Pocket section is rendered"); + + // Test that clicking the recent activity toggle will make the + // recent activity section appear on the newtab page. + // + // We waive XRay wrappers because we want to call the click() + // method defined on the toggle from this context. + let highlightsSwitch = Cu.waiveXrays( + content.document.querySelector("#recent-section moz-toggle") + ); + Assert.ok( + !Services.prefs.getBoolPref(HIGHLIGHTS_PREF), + "Highlights pref is turned off" + ); + Assert.ok(!getSection("highlights"), "Highlights section is not rendered"); + + sectionShownPromise = promiseSectionShown("highlights"); + highlightsSwitch.click(); + await sectionShownPromise; + + Assert.ok(getSection("highlights"), "Highlights section is rendered"); + }, + async after() { + Services.prefs.clearUserPref( + "browser.newtabpage.activity-stream.feeds.topsites" + ); + Services.prefs.clearUserPref( + "browser.newtabpage.activity-stream.feeds.section.topstories" + ); + Services.prefs.clearUserPref( + "browser.newtabpage.activity-stream.feeds.section.highlights" + ); + }, +}); + +test_newtab({ + test: async function test_open_close_customizeMenu() { + const EventUtils = ContentTaskUtils.getEventUtils(content); + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(".personalize-button"), + "Wait for prefs button to load on the newtab page" + ); + + let customizeButton = content.document.querySelector(".personalize-button"); + customizeButton.click(); + + let defaultPos = "matrix(1, 0, 0, 1, 0, 0)"; + await ContentTaskUtils.waitForCondition( + () => + content.getComputedStyle( + content.document.querySelector(".customize-menu") + ).transform === defaultPos, + "Customize Menu should be visible on screen" + ); + + await ContentTaskUtils.waitForCondition( + () => content.document.activeElement.classList.contains("close-button"), + "Close button should be focused when menu becomes visible" + ); + + await ContentTaskUtils.waitForCondition( + () => + content.getComputedStyle( + content.document.querySelector(".personalize-button") + ).visibility === "hidden", + "Personalize button should become hidden" + ); + + // Test close button. + let closeButton = content.document.querySelector(".close-button"); + closeButton.click(); + await ContentTaskUtils.waitForCondition( + () => + content.getComputedStyle( + content.document.querySelector(".customize-menu") + ).transform !== defaultPos, + "Customize Menu should not be visible anymore" + ); + + await ContentTaskUtils.waitForCondition( + () => + content.document.activeElement.classList.contains("personalize-button"), + "Personalize button should be focused when menu closes" + ); + + await ContentTaskUtils.waitForCondition( + () => + content.getComputedStyle( + content.document.querySelector(".personalize-button") + ).visibility === "visible", + "Personalize button should become visible" + ); + + // Reopen the customize menu + customizeButton.click(); + await ContentTaskUtils.waitForCondition( + () => + content.getComputedStyle( + content.document.querySelector(".customize-menu") + ).transform === defaultPos, + "Customize Menu should be visible on screen now" + ); + + // Test closing with esc key. + EventUtils.synthesizeKey("VK_ESCAPE", {}, content); + await ContentTaskUtils.waitForCondition( + () => + content.getComputedStyle( + content.document.querySelector(".customize-menu") + ).transform !== defaultPos, + "Customize Menu should not be visible anymore" + ); + + // Reopen the customize menu + customizeButton.click(); + await ContentTaskUtils.waitForCondition( + () => + content.getComputedStyle( + content.document.querySelector(".customize-menu") + ).transform === defaultPos, + "Customize Menu should be visible on screen now" + ); + + // Test closing with external click. + let outerWrapper = content.document.querySelector(".outer-wrapper"); + outerWrapper.click(); + await ContentTaskUtils.waitForCondition( + () => + content.getComputedStyle( + content.document.querySelector(".customize-menu") + ).transform !== defaultPos, + "Customize Menu should not be visible anymore" + ); + }, +}); diff --git a/browser/components/newtab/test/browser/browser_customize_menu_render.js b/browser/components/newtab/test/browser/browser_customize_menu_render.js new file mode 100644 index 0000000000..6ebd0de7d1 --- /dev/null +++ b/browser/components/newtab/test/browser/browser_customize_menu_render.js @@ -0,0 +1,28 @@ +"use strict"; + +// Test that the customization menu is rendered. +test_newtab({ + test: async function test_render_customizeMenu() { + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(".personalize-button"), + "Wait for personalize button to load on the newtab page" + ); + + let defaultPos = "matrix(1, 0, 0, 1, 0, 0)"; + Assert.notStrictEqual( + content.getComputedStyle( + content.document.querySelector(".customize-menu") + ).transform, + defaultPos, + "Customize Menu should be rendered, but not visible" + ); + + let customizeButton = content.document.querySelector(".personalize-button"); + customizeButton.click(); + + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(".customize-menu"), + "Customize Menu should be rendered now" + ); + }, +}); diff --git a/browser/components/newtab/test/browser/browser_discovery_card.js b/browser/components/newtab/test/browser/browser_discovery_card.js new file mode 100644 index 0000000000..fa90682089 --- /dev/null +++ b/browser/components/newtab/test/browser/browser_discovery_card.js @@ -0,0 +1,49 @@ +// If this fails it could be because of schema changes. +// `topstories.json` defines the stories shown +test_newtab({ + async before({ pushPrefs }) { + sinon + .stub(DiscoveryStreamFeed.prototype, "generateFeedUrl") + .returns( + "https://example.com/browser/browser/components/newtab/test/browser/topstories.json" + ); + await pushPrefs( + [ + "browser.newtabpage.activity-stream.discoverystream.config", + JSON.stringify({ + api_key_pref: "extensions.pocket.oAuthConsumerKey", + collapsible: true, + enabled: true, + personalized: true, + }), + ], + [ + "browser.newtabpage.activity-stream.discoverystream.endpoints", + "https://example.com", + ] + ); + }, + test: async function test_card_render() { + await ContentTaskUtils.waitForCondition( + () => + content.document.querySelectorAll( + "[data-section-id='topstories'] .ds-card-link" + ).length + ); + let found = content.document.querySelectorAll( + "[data-section-id='topstories'] .ds-card-link" + ).length; + is(found, 1, "there should be 1 topstory card"); + let cardPublisher = content.document.querySelector( + "[data-section-id='topstories'] .source" + ).innerText; + is( + cardPublisher, + "bbc", + `Card publisher is ${cardPublisher} instead of bbc` + ); + }, + async after() { + sinon.restore(); + }, +}); diff --git a/browser/components/newtab/test/browser/browser_discovery_render.js b/browser/components/newtab/test/browser/browser_discovery_render.js new file mode 100644 index 0000000000..44ca5b466a --- /dev/null +++ b/browser/components/newtab/test/browser/browser_discovery_render.js @@ -0,0 +1,31 @@ +"use strict"; + +async function before({ pushPrefs }) { + await pushPrefs([ + "browser.newtabpage.activity-stream.discoverystream.config", + JSON.stringify({ + collapsible: true, + enabled: true, + }), + ]); +} + +test_newtab({ + before, + test: async function test_render_hardcoded_topsites() { + const topSites = await ContentTaskUtils.waitForCondition(() => + content.document.querySelector(".ds-top-sites") + ); + ok(topSites, "Got the discovery stream top sites section"); + }, +}); + +test_newtab({ + before, + test: async function test_render_hardcoded_learnmore() { + const learnMoreLink = await ContentTaskUtils.waitForCondition(() => + content.document.querySelector(".ds-layout .learn-more-link > a") + ); + ok(learnMoreLink, "Got the discovery stream learn more link"); + }, +}); diff --git a/browser/components/newtab/test/browser/browser_enabled_newtabpage.js b/browser/components/newtab/test/browser/browser_enabled_newtabpage.js new file mode 100644 index 0000000000..8762160cb1 --- /dev/null +++ b/browser/components/newtab/test/browser/browser_enabled_newtabpage.js @@ -0,0 +1,33 @@ +function getSpec(uri) { + const { spec } = NetUtil.newChannel({ + loadUsingSystemPrincipal: true, + uri, + }).URI; + + info(`got ${spec} for ${uri}`); + return spec; +} + +add_task(async function test_newtab_enabled() { + ok( + !getSpec("about:newtab").endsWith("/blanktab.html"), + "did not get blank for default about:newtab" + ); + ok( + !getSpec("about:home").endsWith("/blanktab.html"), + "did not get blank for default about:home" + ); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.newtabpage.enabled", false]], + }); + + ok( + getSpec("about:newtab").endsWith("/blanktab.html"), + "got special blank page when newtab is not enabled" + ); + ok( + !getSpec("about:home").endsWith("/blanktab.html"), + "got special blank page for about:home" + ); +}); diff --git a/browser/components/newtab/test/browser/browser_foxdoodle_set_default.js b/browser/components/newtab/test/browser/browser_foxdoodle_set_default.js new file mode 100644 index 0000000000..7c53ba6b9a --- /dev/null +++ b/browser/components/newtab/test/browser/browser_foxdoodle_set_default.js @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { ASRouter } = ChromeUtils.importESModule( + "resource:///modules/asrouter/ASRouter.sys.mjs" +); + +const { ASRouterTargeting } = ChromeUtils.importESModule( + "resource:///modules/asrouter/ASRouterTargeting.sys.mjs" +); + +const { OnboardingMessageProvider } = ChromeUtils.importESModule( + "resource:///modules/asrouter/OnboardingMessageProvider.sys.mjs" +); + +async function waitForClick(selector, win) { + await TestUtils.waitForCondition(() => win.document.querySelector(selector)); + win.document.querySelector(selector).click(); +} +add_task(async function test_foxdoodle_spotlight() { + const sandbox = sinon.createSandbox(); + + let promise = TestUtils.topicObserved("subdialog-loaded"); + let message = (await OnboardingMessageProvider.getMessages()).find( + m => m.id === "FOX_DOODLE_SET_DEFAULT" + ); + + Assert.ok(message, "Message exists."); + + let routedMessage = ASRouter.routeCFRMessage( + message, + gBrowser, + undefined, + false + ); + + Assert.ok( + JSON.stringify(routedMessage) === JSON.stringify({ message: {} }), + "Message is not routed when skipInTests is truthy and ID is not present in messagesEnabledInAutomation" + ); + + sandbox + .stub(ASRouter, "messagesEnabledInAutomation") + .value(["FOX_DOODLE_SET_DEFAULT"]); + + routedMessage = ASRouter.routeCFRMessage(message, gBrowser, undefined, false); + Assert.ok( + JSON.stringify(routedMessage.message) === JSON.stringify(message), + "Message is routed when skipInTests is truthy and ID is present in messagesEnabledInAutomation" + ); + + delete message.skipInTests; + let unskippedRoutedMessage = ASRouter.routeCFRMessage( + message, + gBrowser, + undefined, + false + ); + Assert.ok( + unskippedRoutedMessage, + "Message is routed when skipInTests property is falsy" + ); + let [win] = await promise; + await waitForClick("button.dismiss-button", win); + win.close(); + sandbox.restore(); +}); diff --git a/browser/components/newtab/test/browser/browser_getScreenshots.js b/browser/components/newtab/test/browser/browser_getScreenshots.js new file mode 100644 index 0000000000..43e5ec4655 --- /dev/null +++ b/browser/components/newtab/test/browser/browser_getScreenshots.js @@ -0,0 +1,88 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// a blue page +const TEST_URL = + "https://example.com/browser/browser/components/newtab/test/browser/blue_page.html"; +const XHTMLNS = "http://www.w3.org/1999/xhtml"; + +const { Screenshots } = ChromeUtils.importESModule( + "resource://activity-stream/lib/Screenshots.sys.mjs" +); + +function get_pixels(stringOrObject, width, height) { + return new Promise(resolve => { + // get the pixels out of the screenshot that we just took + let img = document.createElementNS(XHTMLNS, "img"); + let imgPath; + + if (typeof stringOrObject === "string") { + Assert.ok( + Services.prefs.getBoolPref( + "browser.tabs.remote.separatePrivilegedContentProcess" + ), + "The privileged about content process should be enabled." + ); + imgPath = stringOrObject; + Assert.ok( + imgPath.startsWith("moz-page-thumb://"), + "Thumbnails should be retrieved using moz-page-thumb://" + ); + } else { + imgPath = URL.createObjectURL(stringOrObject.data); + } + + img.setAttribute("src", imgPath); + img.addEventListener( + "load", + () => { + let canvas = document.createElementNS(XHTMLNS, "canvas"); + canvas.setAttribute("width", width); + canvas.setAttribute("height", height); + let ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0, width, height); + const result = ctx.getImageData(0, 0, width, height).data; + URL.revokeObjectURL(imgPath); + resolve(result); + }, + { once: true } + ); + }); +} + +add_task(async function test_screenshot() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.pagethumbnails.capturing_disabled", false]], + }); + + // take a screenshot of a blue page and save it as a blob + const screenshotAsObject = await Screenshots.getScreenshotForURL(TEST_URL); + let pixels = await get_pixels(screenshotAsObject, 10, 10); + let rgbaCount = { r: 0, g: 0, b: 0, a: 0 }; + while (pixels.length) { + // break the pixels into arrays of 4 components [red, green, blue, alpha] + let [r, g, b, a, ...rest] = pixels; + pixels = rest; + // count the number of each coloured pixels + if (r === 255) { + rgbaCount.r += 1; + } + if (g === 255) { + rgbaCount.g += 1; + } + if (b === 255) { + rgbaCount.b += 1; + } + if (a === 255) { + rgbaCount.a += 1; + } + } + + // in the end, we should only have 100 blue pixels (10 x 10) with full opacity + Assert.equal(rgbaCount.b, 100, "Has 100 blue pixels"); + Assert.equal(rgbaCount.a, 100, "Has full opacity"); + Assert.equal(rgbaCount.r, 0, "Does not have any red pixels"); + Assert.equal(rgbaCount.g, 0, "Does not have any green pixels"); +}); diff --git a/browser/components/newtab/test/browser/browser_highlights_section.js b/browser/components/newtab/test/browser/browser_highlights_section.js new file mode 100644 index 0000000000..d73e4eb361 --- /dev/null +++ b/browser/components/newtab/test/browser/browser_highlights_section.js @@ -0,0 +1,96 @@ +"use strict"; + +/** + * Helper for setup and cleanup of Highlights section tests. + * @param bookmarkCount Number of bookmark higlights to add + * @param test The test case + */ +function test_highlights(bookmarkCount, test) { + test_newtab({ + async before({ tab }) { + if (bookmarkCount) { + await addHighlightsBookmarks(bookmarkCount); + // Wait for HighlightsFeed to update and display the items. + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await ContentTaskUtils.waitForCondition( + () => + content.document.querySelector( + "[data-section-id='highlights'] .card-outer:not(.placeholder)" + ), + "No highlights cards found." + ); + }); + } + }, + test, + async after() { + await clearHistoryAndBookmarks(); + }, + }); +} + +test_highlights( + 2, // Number of highlights cards + function check_highlights_cards() { + let found = content.document.querySelectorAll( + "[data-section-id='highlights'] .card-outer:not(.placeholder)" + ).length; + is(found, 2, "there should be 2 highlights cards"); + + found = content.document.querySelectorAll( + "[data-section-id='highlights'] .section-list .placeholder" + ).length; + is(found, 2, "there should be 1 row * 4 - 2 = 2 highlights placeholder"); + + found = content.document.querySelectorAll( + "[data-section-id='highlights'] .card-context-icon.icon-bookmark-added" + ).length; + is(found, 2, "there should be 2 bookmark icons"); + } +); + +test_highlights( + 1, // Number of highlights cards + function check_highlights_context_menu() { + const menuButton = content.document.querySelector( + "[data-section-id='highlights'] .card-outer .context-menu-button" + ); + // Open the menu. + menuButton.click(); + const found = content.document.querySelector( + "[data-section-id='highlights'] .card-outer .context-menu" + ); + ok(found && !found.hidden, "Should find a visible context menu"); + } +); + +test_highlights( + 1, // Number of highlights cards + async function check_highlights_context_menu() { + const menuButton = content.document.querySelector( + "[data-section-id='highlights'] .card-outer .context-menu-button" + ); + // Open the menu. + menuButton.click(); + const contextMenu = content.document.querySelector( + "[data-section-id='highlights'] .card-outer .context-menu" + ); + ok( + contextMenu && !contextMenu.hidden, + "Should find a visible context menu" + ); + + const removeBookmarkBtn = contextMenu.querySelector( + "[data-section-id='highlights'] button" + ); + removeBookmarkBtn.click(); + + await ContentTaskUtils.waitForCondition( + () => + content.document.querySelectorAll( + "[data-section-id='highlights'] .card-outer:not(.placeholder)" + ), + "no more bookmark cards should be visible" + ); + } +); diff --git a/browser/components/newtab/test/browser/browser_multistage_spotlight.js b/browser/components/newtab/test/browser/browser_multistage_spotlight.js new file mode 100644 index 0000000000..96f655b668 --- /dev/null +++ b/browser/components/newtab/test/browser/browser_multistage_spotlight.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Spotlight } = ChromeUtils.importESModule( + "resource:///modules/asrouter/Spotlight.sys.mjs" +); +const { PanelTestProvider } = ChromeUtils.importESModule( + "resource:///modules/asrouter/PanelTestProvider.sys.mjs" +); +const { SpecialMessageActions } = ChromeUtils.importESModule( + "resource://messaging-system/lib/SpecialMessageActions.sys.mjs" +); + +async function waitForClick(selector, win) { + await TestUtils.waitForCondition(() => win.document.querySelector(selector)); + win.document.querySelector(selector).click(); +} + +async function showDialog(dialogOptions) { + Spotlight.showSpotlightDialog( + dialogOptions.browser, + dialogOptions.message, + dialogOptions.dispatchStub + ); + const [win] = await TestUtils.topicObserved("subdialog-loaded"); + return win; +} + +add_task(async function test_specialAction() { + const sandbox = sinon.createSandbox(); + let message = (await PanelTestProvider.getMessages()).find( + m => m.id === "MULTISTAGE_SPOTLIGHT_MESSAGE" + ); + let dispatchStub = sandbox.stub(); + let browser = BrowserWindowTracker.getTopWindow().gBrowser.selectedBrowser; + let specialActionStub = sandbox.stub(SpecialMessageActions, "handleAction"); + + let win = await showDialog({ message, browser, dispatchStub }); + await waitForClick("button.primary", win); + win.close(); + + Assert.equal( + specialActionStub.callCount, + 1, + "Should be called by primary action" + ); + Assert.deepEqual( + specialActionStub.firstCall.args[0], + message.content.screens[0].content.primary_button.action, + "Should be called with button action" + ); + + sandbox.restore(); +}); + +add_task(async function test_embedded_import() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.migrate.internal-testing.enabled", true]], + }); + let message = (await PanelTestProvider.getMessages()).find( + m => m.id === "IMPORT_SETTINGS_EMBEDDED" + ); + let browser = BrowserWindowTracker.getTopWindow().gBrowser.selectedBrowser; + let win = await showDialog({ message, browser }); + let migrationWizardReady = BrowserTestUtils.waitForEvent( + win, + "MigrationWizard:Ready" + ); + + await TestUtils.waitForCondition(() => + win.document.querySelector("migration-wizard") + ); + Assert.ok( + win.document.querySelector("migration-wizard"), + "Migration Wizard rendered" + ); + + await migrationWizardReady; + + let panelList = win.document + .querySelector("migration-wizard") + .openOrClosedShadowRoot.querySelector("panel-list"); + Assert.equal(panelList.tagName, "PANEL-LIST"); + Assert.equal(panelList.firstChild.tagName, "PANEL-ITEM"); + + win.close(); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/newtab/test/browser/browser_multistage_spotlight_telemetry.js b/browser/components/newtab/test/browser/browser_multistage_spotlight_telemetry.js new file mode 100644 index 0000000000..03eb6caddd --- /dev/null +++ b/browser/components/newtab/test/browser/browser_multistage_spotlight_telemetry.js @@ -0,0 +1,141 @@ +"use strict"; + +const { Spotlight } = ChromeUtils.importESModule( + "resource:///modules/asrouter/Spotlight.sys.mjs" +); +const { PanelTestProvider } = ChromeUtils.importESModule( + "resource:///modules/asrouter/PanelTestProvider.sys.mjs" +); + +const { AboutWelcomeTelemetry } = ChromeUtils.importESModule( + "resource:///modules/aboutwelcome/AboutWelcomeTelemetry.sys.mjs" +); + +async function waitForClick(selector, win) { + await TestUtils.waitForCondition(() => win.document.querySelector(selector)); + win.document.querySelector(selector).click(); +} + +function waitForDialog(callback = win => win.close()) { + return BrowserTestUtils.promiseAlertDialog( + null, + "chrome://browser/content/spotlight.html", + { callback, isSubDialog: true } + ); +} + +function showAndWaitForDialog(dialogOptions, callback) { + const promise = waitForDialog(callback); + Spotlight.showSpotlightDialog( + dialogOptions.browser, + dialogOptions.message, + dialogOptions.dispatchStub + ); + return promise; +} + +add_task(async function send_spotlight_as_page_in_telemetry() { + let message = (await PanelTestProvider.getMessages()).find( + m => m.id === "MULTISTAGE_SPOTLIGHT_MESSAGE" + ); + let dispatchStub = sinon.stub(); + let browser = BrowserWindowTracker.getTopWindow().gBrowser.selectedBrowser; + let sandbox = sinon.createSandbox(); + + await showAndWaitForDialog({ message, browser, dispatchStub }, async win => { + let stub = sandbox.stub(win, "AWSendEventTelemetry"); + await waitForClick("button.secondary", win); + Assert.equal( + stub.lastCall.args[0].event_context.page, + "spotlight", + "The value of event context page should be set to 'spotlight' in event telemetry" + ); + win.close(); + }); + + sandbox.restore(); +}); + +add_task(async function send_dismiss_event_telemetry() { + // Have to turn on AS telemetry for anything to be recorded. + await SpecialPowers.pushPrefEnv({ + set: [["browser.newtabpage.activity-stream.telemetry", true]], + }); + registerCleanupFunction(async () => { + await SpecialPowers.popPrefEnv(); + }); + + // Let's collect all "messaging-system" pings submitted in this test. + let pingContents = []; + let onSubmit = () => { + pingContents.push({ + messageId: Glean.messagingSystem.messageId.testGetValue(), + event: Glean.messagingSystem.event.testGetValue(), + }); + GleanPings.messagingSystem.testBeforeNextSubmit(onSubmit); + }; + GleanPings.messagingSystem.testBeforeNextSubmit(onSubmit); + + const messageId = "MULTISTAGE_SPOTLIGHT_MESSAGE"; + let message = (await PanelTestProvider.getMessages()).find( + m => m.id === messageId + ); + let browser = BrowserWindowTracker.getTopWindow().gBrowser.selectedBrowser; + let sandbox = sinon.createSandbox(); + let spy = sandbox.spy(AboutWelcomeTelemetry.prototype, "sendTelemetry"); + // send without a dispatch function so that default is used + await showAndWaitForDialog({ message, browser }, async win => { + await waitForClick("button.dismiss-button", win); + await win.close(); + }); + + Assert.equal( + spy.lastCall.args[0].message_id, + messageId, + "A dismiss event is called with the correct message id" + ); + + Assert.equal( + spy.lastCall.args[0].event, + "DISMISS", + "A dismiss event is called with a top level event field with value 'DISMISS'" + ); + + Assert.greater( + pingContents.length, + 0, + "Glean 'messaging-system' pings were submitted." + ); + Assert.ok( + pingContents.some(ping => { + return ping.messageId === messageId && ping.event === "DISMISS"; + }), + "A Glean 'messaging-system' ping was sent for the correct message+event." + ); + + // Tidy up by removing the self-referential test callback. + GleanPings.messagingSystem.testBeforeNextSubmit(() => {}); + sandbox.restore(); +}); + +add_task( + async function do_not_send_impression_telemetry_from_default_dispatch() { + // Don't send impression telemetry from the Spotlight default dispatch function + let message = (await PanelTestProvider.getMessages()).find( + m => m.id === "MULTISTAGE_SPOTLIGHT_MESSAGE" + ); + let browser = BrowserWindowTracker.getTopWindow().gBrowser.selectedBrowser; + let sandbox = sinon.createSandbox(); + let stub = sandbox.stub(AboutWelcomeTelemetry.prototype, "sendTelemetry"); + // send without a dispatch function so that default is used + await showAndWaitForDialog({ message, browser }); + + Assert.equal( + stub.calledOn(), + false, + "No extra impression event was sent for multistage Spotlight" + ); + + sandbox.restore(); + } +); diff --git a/browser/components/newtab/test/browser/browser_newtab_glean.js b/browser/components/newtab/test/browser/browser_newtab_glean.js new file mode 100644 index 0000000000..7d40868f2c --- /dev/null +++ b/browser/components/newtab/test/browser/browser_newtab_glean.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(10); + +const TELEMETRY_PREF = + "browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar"; + +add_task(async function test_newtab_handoff_performance_telemetry() { + await SpecialPowers.pushPrefEnv({ + set: [[TELEMETRY_PREF, true]], + }); + + Services.fog.testResetFOG(); + let TelemetryFeed = + AboutNewTab.activityStream.store.feeds.get("feeds.telemetry"); + TelemetryFeed.init(); // INIT action doesn't happen by default. + + Assert.equal(true, Glean.newtabHandoffPreference.enabled.testGetValue()); + + await SpecialPowers.pushPrefEnv({ + set: [[TELEMETRY_PREF, false]], + }); + Assert.equal(false, Glean.newtabHandoffPreference.enabled.testGetValue()); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/newtab/test/browser/browser_newtab_header.js b/browser/components/newtab/test/browser/browser_newtab_header.js new file mode 100644 index 0000000000..adfecbe71f --- /dev/null +++ b/browser/components/newtab/test/browser/browser_newtab_header.js @@ -0,0 +1,76 @@ +"use strict"; + +// Tests that: +// 1. Top sites header is hidden and the topsites section is not collapsed on load. +// 2. Pocket header and section are visible and not collapsed on load. +// 3. Recent activity section and header are visible and not collapsed on load. +test_newtab({ + test: async function test_render_customizeMenu() { + // Top sites section + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(".top-sites"), + "Wait for the top sites section to load" + ); + + let topSitesSection = content.document.querySelector(".top-sites"); + let titleContainer = topSitesSection.querySelector( + ".section-title-container" + ); + ok( + titleContainer && titleContainer.style.visibility === "hidden", + "Top sites header should not be visible" + ); + + let isTopSitesCollapsed = topSitesSection.className.includes("collapsed"); + ok(!isTopSitesCollapsed, "Top sites should not be collapsed on load"); + + // Pocket section + await ContentTaskUtils.waitForCondition( + () => + content.document.querySelector("section[data-section-id='topstories']"), + "Wait for the pocket section to load" + ); + + let pocketSection = content.document.querySelector( + "section[data-section-id='topstories']" + ); + let isPocketSectionCollapsed = + pocketSection.className.includes("collapsed"); + ok( + !isPocketSectionCollapsed, + "Pocket section should not be collapsed on load" + ); + + let pocketHeader = content.document.querySelector( + "section[data-section-id='topstories'] .section-title" + ); + ok( + pocketHeader && !pocketHeader.style.visibility, + "Pocket header should be visible" + ); + + // Highlights (Recent activity) section. + await ContentTaskUtils.waitForCondition( + () => + content.document.querySelector("section[data-section-id='highlights']"), + "Wait for the highlights section to load" + ); + let highlightsSection = content.document.querySelector( + "section[data-section-id='topstories']" + ); + let isHighlightsSectionCollapsed = + highlightsSection.className.includes("collapsed"); + ok( + !isHighlightsSectionCollapsed, + "Highlights section should not be collapsed on load" + ); + + let highlightsHeader = content.document.querySelector( + "section[data-section-id='highlights'] .section-title" + ); + ok( + highlightsHeader && !highlightsHeader.style.visibility, + "Highlights header should be visible" + ); + }, +}); diff --git a/browser/components/newtab/test/browser/browser_newtab_last_LinkMenu.js b/browser/components/newtab/test/browser/browser_newtab_last_LinkMenu.js new file mode 100644 index 0000000000..d9264fdf7c --- /dev/null +++ b/browser/components/newtab/test/browser/browser_newtab_last_LinkMenu.js @@ -0,0 +1,153 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +async function setupPrefs() { + sinon + .stub(DiscoveryStreamFeed.prototype, "generateFeedUrl") + .returns( + "https://example.com/browser/browser/components/newtab/test/browser/topstories.json" + ); + await setDefaultTopSites(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.newtabpage.activity-stream.discoverystream.config", + JSON.stringify({ + api_key_pref: "extensions.pocket.oAuthConsumerKey", + collapsible: true, + enabled: true, + personalized: false, + }), + ], + [ + "browser.newtabpage.activity-stream.discoverystream.endpoints", + "https://example.com", + ], + ], + }); +} + +async function resetPrefs() { + // We set 5 prefs in setupPrefs, so we should reset 5 prefs. + // 1 popPrefEnv from pushPrefEnv + // and 4 popPrefEnv happen internally in setDefaultTopSites. + await SpecialPowers.popPrefEnv(); + await SpecialPowers.popPrefEnv(); + await SpecialPowers.popPrefEnv(); + await SpecialPowers.popPrefEnv(); + await SpecialPowers.popPrefEnv(); +} + +let initialHeight; +let initialWidth; +function setSize(width, height) { + initialHeight = window.innerHeight; + initialWidth = window.innerWidth; + let resizePromise = BrowserTestUtils.waitForEvent(window, "resize", false); + window.resizeTo(width, height); + return resizePromise; +} + +function resetSize() { + let resizePromise = BrowserTestUtils.waitForEvent(window, "resize", false); + window.resizeTo(initialWidth, initialHeight); + return resizePromise; +} + +add_task(async function test_newtab_last_LinkMenu() { + await setupPrefs(); + + // Open about:newtab without using the default load listener + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:newtab", + false + ); + + // Specially wait for potentially preloaded browsers + let browser = tab.linkedBrowser; + await waitForPreloaded(browser); + + // Wait for React to render something + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => content.document.getElementById("root").children.length + ), + "Should render activity stream content" + ); + + // Set the window to a small enough size to trigger menus that might overflow. + await setSize(600, 450); + + // Test context menu position for topsites. + await SpecialPowers.spawn(browser, [], async () => { + // Topsites might not be ready, so wait for the button. + await ContentTaskUtils.waitForCondition( + () => + content.document.querySelector( + ".top-site-outer:nth-child(2n) .context-menu-button" + ), + "Wait for the Pocket card and button" + ); + const topsiteOuter = content.document.querySelector( + ".top-site-outer:nth-child(2n)" + ); + const topsiteContextMenuButton = topsiteOuter.querySelector( + ".context-menu-button" + ); + + topsiteContextMenuButton.click(); + + await ContentTaskUtils.waitForCondition( + () => topsiteOuter.classList.contains("active"), + "Wait for the menu to be active" + ); + + is( + content.window.scrollMaxX, + 0, + "there should be no horizontal scroll bar" + ); + }); + + // Test context menu position for topstories. + await SpecialPowers.spawn(browser, [], async () => { + // Pocket section might take a bit more time to load, + // so wait for the button to be ready. + await ContentTaskUtils.waitForCondition( + () => + content.document.querySelector( + ".ds-card:nth-child(1n) .context-menu-button" + ), + "Wait for the Pocket card and button" + ); + + const dsCard = content.document.querySelector(".ds-card:nth-child(1n)"); + const dsCarContextMenuButton = dsCard.querySelector(".context-menu-button"); + + dsCarContextMenuButton.click(); + + await ContentTaskUtils.waitForCondition( + () => dsCard.classList.contains("active"), + "Wait for the menu to be active" + ); + + is( + content.window.scrollMaxX, + 0, + "there should be no horizontal scroll bar" + ); + }); + + // Resetting the window size to what it was. + await resetSize(); + // Resetting prefs we set for this test. + await resetPrefs(); + BrowserTestUtils.removeTab(tab); + sinon.restore(); +}); diff --git a/browser/components/newtab/test/browser/browser_newtab_overrides.js b/browser/components/newtab/test/browser/browser_newtab_overrides.js new file mode 100644 index 0000000000..1d4a0c36e3 --- /dev/null +++ b/browser/components/newtab/test/browser/browser_newtab_overrides.js @@ -0,0 +1,134 @@ +"use strict"; + +registerCleanupFunction(() => { + AboutNewTab.resetNewTabURL(); +}); + +function nextChangeNotificationPromise(aNewURL, testMessage) { + return TestUtils.topicObserved( + "newtab-url-changed", + function observer(aSubject, aData) { + Assert.equal(aData, aNewURL, testMessage); + return true; + } + ); +} + +/* + * Tests that the default newtab page is always returned when one types "about:newtab" in the URL bar, + * even when overridden. + */ +add_task(async function redirector_ignores_override() { + let overrides = ["chrome://browser/content/aboutRobots.xhtml", "about:home"]; + + for (let overrideURL of overrides) { + let notificationPromise = nextChangeNotificationPromise( + overrideURL, + `newtab page now points to ${overrideURL}` + ); + AboutNewTab.newTabURL = overrideURL; + + await notificationPromise; + Assert.ok(AboutNewTab.newTabURLOverridden, "url has been overridden"); + + let tabOptions = { + gBrowser, + url: "about:newtab", + }; + + /* + * Simulate typing "about:newtab" in the url bar. + * + * Bug 1240169 - We expect the redirector to lead the user to "about:newtab", the default URL, + * due to invoking AboutRedirector. A user interacting with the chrome otherwise would lead + * to the overriding URLs. + */ + await BrowserTestUtils.withNewTab(tabOptions, async browser => { + await SpecialPowers.spawn(browser, [], async () => { + Assert.equal(content.location.href, "about:newtab", "Got right URL"); + Assert.equal( + content.document.location.href, + "about:newtab", + "Got right URL" + ); + Assert.notEqual( + content.document.nodePrincipal, + Services.scriptSecurityManager.getSystemPrincipal(), + "activity stream principal should not match systemPrincipal" + ); + }); + }); + } +}); + +/* + * Tests loading an overridden newtab page by simulating opening a newtab page from chrome + */ +add_task(async function override_loads_in_browser() { + let overrides = [ + "chrome://browser/content/aboutRobots.xhtml", + "about:home", + " about:home", + ]; + + for (let overrideURL of overrides) { + let notificationPromise = nextChangeNotificationPromise( + overrideURL.trim(), + `newtab page now points to ${overrideURL}` + ); + AboutNewTab.newTabURL = overrideURL; + + await notificationPromise; + Assert.ok(AboutNewTab.newTabURLOverridden, "url has been overridden"); + + // simulate a newtab open as a user would + BrowserOpenTab(); + + let browser = gBrowser.selectedBrowser; + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn(browser, [{ url: overrideURL }], async args => { + Assert.equal(content.location.href, args.url.trim(), "Got right URL"); + Assert.equal( + content.document.location.href, + args.url.trim(), + "Got right URL" + ); + }); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } +}); + +/* + * Tests edge cases when someone overrides the newtabpage with whitespace + */ +add_task(async function override_blank_loads_in_browser() { + let overrides = ["", " ", "\n\t", " about:blank"]; + + for (let overrideURL of overrides) { + let notificationPromise = nextChangeNotificationPromise( + "about:blank", + "newtab page now points to about:blank" + ); + AboutNewTab.newTabURL = overrideURL; + + await notificationPromise; + Assert.ok(AboutNewTab.newTabURLOverridden, "url has been overridden"); + + // simulate a newtab open as a user would + BrowserOpenTab(); + + let browser = gBrowser.selectedBrowser; + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn(browser, [], async () => { + Assert.equal(content.location.href, "about:blank", "Got right URL"); + Assert.equal( + content.document.location.href, + "about:blank", + "Got right URL" + ); + }); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } +}); diff --git a/browser/components/newtab/test/browser/browser_newtab_ping.js b/browser/components/newtab/test/browser/browser_newtab_ping.js new file mode 100644 index 0000000000..009305eb7a --- /dev/null +++ b/browser/components/newtab/test/browser/browser_newtab_ping.js @@ -0,0 +1,216 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +requestLongerTimeout(5); + +const { ASRouter } = ChromeUtils.importESModule( + "resource:///modules/asrouter/ASRouter.sys.mjs" +); + +const { ExperimentFakes } = ChromeUtils.importESModule( + "resource://testing-common/NimbusTestUtils.sys.mjs" +); + +let sendTriggerMessageSpy; + +add_setup(function () { + let sandbox = sinon.createSandbox(); + sendTriggerMessageSpy = sandbox.spy(ASRouter, "sendTriggerMessage"); + + registerCleanupFunction(() => { + sandbox.restore(); + }); +}); + +add_task(async function test_newtab_tab_close_sends_ping() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.newtabpage.activity-stream.telemetry", true]], + }); + + Services.fog.testResetFOG(); + sendTriggerMessageSpy.resetHistory(); + let TelemetryFeed = + AboutNewTab.activityStream.store.feeds.get("feeds.telemetry"); + TelemetryFeed.init(); // INIT action doesn't happen by default. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:newtab", + false // waitForLoad; about:newtab is cached so this would never resolve + ); + + await BrowserTestUtils.waitForCondition( + () => sendTriggerMessageSpy.called, + "After about:newtab finishes loading" + ); + sendTriggerMessageSpy.resetHistory(); + + await BrowserTestUtils.waitForCondition( + () => !!Glean.newtab.opened.testGetValue("newtab"), + "We expect the newtab open to be recorded" + ); + let record = Glean.newtab.opened.testGetValue("newtab"); + Assert.equal(record.length, 1, "Should only be one open"); + const sessionId = record[0].extra.newtab_visit_id; + Assert.ok(!!sessionId, "newtab_visit_id must be present"); + + let pingSubmitted = false; + GleanPings.newtab.testBeforeNextSubmit(reason => { + pingSubmitted = true; + Assert.equal(reason, "newtab_session_end"); + record = Glean.newtab.closed.testGetValue("newtab"); + Assert.equal(record.length, 1, "Should only have one close"); + Assert.equal( + record[0].extra.newtab_visit_id, + sessionId, + "Should've closed the session we opened" + ); + Assert.ok(Glean.newtabSearch.enabled.testGetValue()); + Assert.ok(Glean.topsites.enabled.testGetValue()); + // Sponsored topsites are turned off in tests to avoid making remote requests. + Assert.ok(!Glean.topsites.sponsoredEnabled.testGetValue()); + Assert.ok(Glean.pocket.enabled.testGetValue()); + Assert.ok(Glean.pocket.sponsoredStoriesEnabled.testGetValue()); + Assert.equal(false, Glean.pocket.isSignedIn.testGetValue()); + }); + + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.waitForCondition( + () => pingSubmitted, + "We expect the ping to have submitted." + ); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_newtab_tab_nav_sends_ping() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.newtabpage.activity-stream.telemetry", true]], + }); + + Services.fog.testResetFOG(); + sendTriggerMessageSpy.resetHistory(); + let TelemetryFeed = + AboutNewTab.activityStream.store.feeds.get("feeds.telemetry"); + TelemetryFeed.init(); // INIT action doesn't happen by default. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:newtab", + false // waitForLoad; about:newtab is cached so this would never resolve + ); + + await BrowserTestUtils.waitForCondition( + () => sendTriggerMessageSpy.called, + "After about:newtab finishes loading" + ); + sendTriggerMessageSpy.resetHistory(); + + await BrowserTestUtils.waitForCondition( + () => !!Glean.newtab.opened.testGetValue("newtab"), + "We expect the newtab open to be recorded" + ); + let record = Glean.newtab.opened.testGetValue("newtab"); + Assert.equal(record.length, 1, "Should only be one open"); + const sessionId = record[0].extra.newtab_visit_id; + Assert.ok(!!sessionId, "newtab_visit_id must be present"); + + let pingSubmitted = false; + GleanPings.newtab.testBeforeNextSubmit(reason => { + pingSubmitted = true; + Assert.equal(reason, "newtab_session_end"); + record = Glean.newtab.closed.testGetValue("newtab"); + Assert.equal(record.length, 1, "Should only have one close"); + Assert.equal( + record[0].extra.newtab_visit_id, + sessionId, + "Should've closed the session we opened" + ); + Assert.ok(Glean.newtabSearch.enabled.testGetValue()); + Assert.ok(Glean.topsites.enabled.testGetValue()); + // Sponsored topsites are turned off in tests to avoid making remote requests. + Assert.ok(!Glean.topsites.sponsoredEnabled.testGetValue()); + Assert.ok(Glean.pocket.enabled.testGetValue()); + Assert.ok(Glean.pocket.sponsoredStoriesEnabled.testGetValue()); + Assert.equal(false, Glean.pocket.isSignedIn.testGetValue()); + }); + + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, "about:mozilla"); + await BrowserTestUtils.waitForCondition( + () => pingSubmitted, + "We expect the ping to have submitted." + ); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_newtab_doesnt_send_nimbus() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.newtabpage.activity-stream.telemetry", true]], + }); + + let doEnrollmentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "glean", + value: { newtabPingEnabled: false }, + }); + Services.fog.testResetFOG(); + let TelemetryFeed = + AboutNewTab.activityStream.store.feeds.get("feeds.telemetry"); + TelemetryFeed.init(); // INIT action doesn't happen by default. + sendTriggerMessageSpy.resetHistory(); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:newtab", + false // waitForLoad; about:newtab is cached so this would never resolve + ); + + await BrowserTestUtils.waitForCondition( + () => sendTriggerMessageSpy.called, + "After about:newtab finishes loading" + ); + sendTriggerMessageSpy.resetHistory(); + + await BrowserTestUtils.waitForCondition( + () => !!Glean.newtab.opened.testGetValue("newtab"), + "We expect the newtab open to be recorded" + ); + let record = Glean.newtab.opened.testGetValue("newtab"); + Assert.equal(record.length, 1, "Should only be one open"); + const sessionId = record[0].extra.newtab_visit_id; + Assert.ok(!!sessionId, "newtab_visit_id must be present"); + + GleanPings.newtab.testBeforeNextSubmit(() => { + Assert.ok(false, "Must not submit ping!"); + }); + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, "about:mozilla"); + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.waitForCondition(() => { + let { sessions } = + AboutNewTab.activityStream.store.feeds.get("feeds.telemetry"); + return !Array.from(sessions.entries()).filter( + ([k, v]) => v.session_id === sessionId + ).length; + }, "Waiting for sessions to clean up."); + // Session ended without a ping being sent. Success! + await doEnrollmentCleanup(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_newtab_categorization_sends_ping() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.newtabpage.activity-stream.telemetry", true]], + }); + + Services.fog.testResetFOG(); + sendTriggerMessageSpy.resetHistory(); + let TelemetryFeed = + AboutNewTab.activityStream.store.feeds.get("feeds.telemetry"); + let pingSent = false; + GleanPings.newtab.testBeforeNextSubmit(reason => { + pingSent = true; + Assert.equal(reason, "component_init"); + }); + await TelemetryFeed.sendPageTakeoverData(); + Assert.ok(pingSent, "ping was sent"); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/newtab/test/browser/browser_newtab_towindow.js b/browser/components/newtab/test/browser/browser_newtab_towindow.js new file mode 100644 index 0000000000..d0a49e63f0 --- /dev/null +++ b/browser/components/newtab/test/browser/browser_newtab_towindow.js @@ -0,0 +1,45 @@ +// This test simulates opening the newtab page and moving it to a new window. +// Links in the page should still work. +add_task(async function test_newtab_to_window() { + await setTestTopSites(); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:newtab", + false + ); + + let swappedPromise = BrowserTestUtils.waitForEvent( + tab.linkedBrowser, + "SwapDocShells" + ); + let newWindow = gBrowser.replaceTabWithWindow(tab); + await swappedPromise; + + is( + newWindow.gBrowser.selectedBrowser.currentURI.spec, + "about:newtab", + "about:newtab moved to window" + ); + + let tabPromise = BrowserTestUtils.waitForNewTab( + newWindow.gBrowser, + "https://example.com/", + true + ); + + await BrowserTestUtils.synthesizeMouse( + `.top-sites a`, + 2, + 2, + { accelKey: true }, + newWindow.gBrowser.selectedBrowser + ); + + await tabPromise; + + is(newWindow.gBrowser.tabs.length, 2, "second page is opened"); + + BrowserTestUtils.removeTab(newWindow.gBrowser.selectedTab); + await BrowserTestUtils.closeWindow(newWindow); +}); diff --git a/browser/components/newtab/test/browser/browser_newtab_trigger.js b/browser/components/newtab/test/browser/browser_newtab_trigger.js new file mode 100644 index 0000000000..b18da77ec6 --- /dev/null +++ b/browser/components/newtab/test/browser/browser_newtab_trigger.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ASRouter } = ChromeUtils.importESModule( + "resource:///modules/asrouter/ASRouter.sys.mjs" +); + +let sendTriggerMessageSpy; +let triggerMatch; + +add_setup(function () { + let sandbox = sinon.createSandbox(); + sendTriggerMessageSpy = sandbox.spy(ASRouter, "sendTriggerMessage"); + triggerMatch = sandbox.match({ id: "defaultBrowserCheck" }); + + registerCleanupFunction(() => { + sandbox.restore(); + }); +}); + +async function testPageTrigger(url, waitForLoad, expectedTrigger) { + sendTriggerMessageSpy.resetHistory(); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + url, + waitForLoad + ); + + await BrowserTestUtils.waitForCondition( + () => sendTriggerMessageSpy.calledWith(expectedTrigger), + `After ${url} finishes loading` + ); + Assert.ok( + sendTriggerMessageSpy.calledWith(expectedTrigger), + `Found the expected ${expectedTrigger.id} trigger` + ); + + BrowserTestUtils.removeTab(tab); + sendTriggerMessageSpy.resetHistory(); +} + +add_task(function test_newtab_trigger() { + return testPageTrigger("about:newtab", false, triggerMatch); +}); + +add_task(function test_abouthome_trigger() { + return testPageTrigger("about:home", true, triggerMatch); +}); diff --git a/browser/components/newtab/test/browser/browser_open_tab_focus.js b/browser/components/newtab/test/browser/browser_open_tab_focus.js new file mode 100644 index 0000000000..265d713371 --- /dev/null +++ b/browser/components/newtab/test/browser/browser_open_tab_focus.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_open_tab_focus() { + await setTestTopSites(); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:newtab", + false + ); + // Specially wait for potentially preloaded browsers + let browser = tab.linkedBrowser; + await waitForPreloaded(browser); + // Wait for React to render something + await SpecialPowers.spawn(browser, [], async () => { + await ContentTaskUtils.waitForCondition(() => + content.document.querySelector(".top-sites-list .top-site-button .title") + ); + }); + + await BrowserTestUtils.synthesizeMouse( + `.top-sites-list .top-site-button .title`, + 2, + 2, + { accelKey: true }, + browser + ); + + Assert.strictEqual( + gBrowser.selectedTab, + tab, + "The original tab is still the selected tab" + ); + BrowserTestUtils.removeTab(gBrowser.tabs[2]); // example.org tab + BrowserTestUtils.removeTab(tab); // The original tab +}); diff --git a/browser/components/newtab/test/browser/browser_remote_l10n.js b/browser/components/newtab/test/browser/browser_remote_l10n.js new file mode 100644 index 0000000000..5c919357f4 --- /dev/null +++ b/browser/components/newtab/test/browser/browser_remote_l10n.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { RemoteL10n } = ChromeUtils.importESModule( + "resource:///modules/asrouter/RemoteL10n.sys.mjs" +); + +const ID = "remote_l10n_test_string"; +const VALUE = "RemoteL10n string"; +const CONTENT = `${ID} = ${VALUE}`; + +add_setup(async () => { + const l10nRegistryInstance = L10nRegistry.getInstance(); + const localProfileDir = Services.dirsvc.get("ProfD", Ci.nsIFile).path; + const dirPath = PathUtils.join( + localProfileDir, + ...["settings", "main", "ms-language-packs", "browser", "newtab"] + ); + const filePath = PathUtils.join(dirPath, "asrouter.ftl"); + + await IOUtils.makeDirectory(dirPath, { + ignoreExisting: true, + from: localProfileDir, + }); + await IOUtils.writeUTF8(filePath, CONTENT, { + tmpPath: `${filePath}.tmp`, + }); + + // Remove any cached l10n resources, "cfr" is the cache key + // used for strings from the remote `asrouter.ftl` see RemoteL10n.sys.mjs + RemoteL10n.reloadL10n(); + if (l10nRegistryInstance.hasSource("cfr")) { + l10nRegistryInstance.removeSources(["cfr"]); + } +}); + +add_task(async function test_TODO() { + let [{ value }] = await RemoteL10n.l10n.formatMessages([{ id: ID }]); + + Assert.equal(value, VALUE, "Got back the string we wrote to disk"); +}); + +// Test that the formatting helper works. This helper is lower-level than the +// DOM localization apparatus, and as such doesn't require the weight of the +// `browser` test framework, but it's nice to co-locate related tests. +add_task(async function test_formatLocalizableText() { + let value = await RemoteL10n.formatLocalizableText({ string_id: ID }); + + Assert.equal(value, VALUE, "Got back the string we wrote to disk"); + + value = await RemoteL10n.formatLocalizableText("unchanged"); + + Assert.equal(value, "unchanged", "Got back the string provided"); +}); diff --git a/browser/components/newtab/test/browser/browser_topsites_annotation.js b/browser/components/newtab/test/browser/browser_topsites_annotation.js new file mode 100644 index 0000000000..7e48868fca --- /dev/null +++ b/browser/components/newtab/test/browser/browser_topsites_annotation.js @@ -0,0 +1,980 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test whether a visit information is annotated correctly when clicking a tile. + +if (AppConstants.platform === "macosx") { + requestLongerTimeout(4); +} else { + requestLongerTimeout(2); +} + +ChromeUtils.defineESModuleGetters(this, { + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +const OPEN_TYPE = { + CURRENT_BY_CLICK: 0, + NEWTAB_BY_CLICK: 1, + NEWTAB_BY_MIDDLECLICK: 2, + NEWTAB_BY_CONTEXTMENU: 3, + NEWWINDOW_BY_CONTEXTMENU: 4, + NEWWINDOW_BY_CONTEXTMENU_OF_TILE: 5, +}; + +const FRECENCY = { + TYPED: 2000, + VISITED: 100, + SPONSORED: -1, + BOOKMARKED: 2075, + MIDDLECLICK_TYPED: 100, + MIDDLECLICK_BOOKMARKED: 175, + NEWWINDOW_TYPED: 100, + NEWWINDOW_BOOKMARKED: 175, +}; + +const { + VISIT_SOURCE_ORGANIC, + VISIT_SOURCE_SPONSORED, + VISIT_SOURCE_BOOKMARKED, +} = PlacesUtils.history; + +/** + * To be used before checking database contents when they depend on a visit + * being added to History. + * @param {string} href the page to await notifications for. + */ +async function waitForVisitNotification(href) { + await PlacesTestUtils.waitForNotification("page-visited", events => + events.some(e => e.url === href) + ); +} + +async function assertDatabase({ targetURL, expected }) { + const frecency = await PlacesTestUtils.getDatabaseValue( + "moz_places", + "frecency", + { url: targetURL } + ); + Assert.equal(frecency, expected.frecency, "Frecency is correct"); + + const placesId = await PlacesTestUtils.getDatabaseValue("moz_places", "id", { + url: targetURL, + }); + const expectedTriggeringPlaceId = expected.triggerURL + ? await PlacesTestUtils.getDatabaseValue("moz_places", "id", { + url: expected.triggerURL, + }) + : null; + const db = await PlacesUtils.promiseDBConnection(); + const rows = await db.execute( + "SELECT source, triggeringPlaceId FROM moz_historyvisits WHERE place_id = :place_id AND source = :source", + { + place_id: placesId, + source: expected.source, + } + ); + Assert.equal(rows.length, 1); + Assert.equal( + rows[0].getResultByName("triggeringPlaceId"), + expectedTriggeringPlaceId, + `The triggeringPlaceId in database is correct for ${targetURL}` + ); +} + +async function waitForLocationChanged(destinationURL) { + // If nodeIconChanged of browserPlacesViews.js is called after the target node + // is lost during test, "No DOM node set for aPlacesNode" error occur. To avoid + // this failure, wait for the onLocationChange event that triggers + // nodeIconChanged to occur. + return new Promise(resolve => { + gBrowser.addTabsProgressListener({ + async onLocationChange(aBrowser, aWebProgress, aRequest, aLocation) { + if (aLocation.spec === destinationURL) { + gBrowser.removeTabsProgressListener(this); + // Wait for an empty Promise to ensure to proceed our test after + // finishing the processing of other onLocatoinChanged events. + await Promise.resolve(); + resolve(); + } + }, + }); + }); +} + +async function openAndTest({ + linkSelector, + linkURL, + redirectTo = null, + openType = OPEN_TYPE.CURRENT_BY_CLICK, + expected, +}) { + const destinationURL = redirectTo || linkURL; + + // Wait for content is ready. + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [linkSelector, linkURL], + async (selector, link) => { + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(selector).href === link + ); + } + ); + + info("Open specific link by type and wait for loading."); + let promiseVisited = waitForVisitNotification(destinationURL); + if (openType === OPEN_TYPE.CURRENT_BY_CLICK) { + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + destinationURL + ); + const onLocationChanged = waitForLocationChanged(destinationURL); + + await BrowserTestUtils.synthesizeMouseAtCenter( + linkSelector, + {}, + gBrowser.selectedBrowser + ); + + await onLoad; + await onLocationChanged; + } else if (openType === OPEN_TYPE.NEWTAB_BY_CLICK) { + const onLoad = BrowserTestUtils.waitForNewTab( + gBrowser, + destinationURL, + true + ); + const onLocationChanged = waitForLocationChanged(destinationURL); + + await BrowserTestUtils.synthesizeMouseAtCenter( + linkSelector, + { ctrlKey: true, metaKey: true }, + gBrowser.selectedBrowser + ); + + const tab = await onLoad; + await onLocationChanged; + BrowserTestUtils.removeTab(tab); + } else if (openType === OPEN_TYPE.NEWTAB_BY_MIDDLECLICK) { + const onLoad = BrowserTestUtils.waitForNewTab( + gBrowser, + destinationURL, + true + ); + const onLocationChanged = waitForLocationChanged(destinationURL); + + await BrowserTestUtils.synthesizeMouseAtCenter( + linkSelector, + { button: 1 }, + gBrowser.selectedBrowser + ); + + const tab = await onLoad; + await onLocationChanged; + BrowserTestUtils.removeTab(tab); + } else if (openType === OPEN_TYPE.NEWTAB_BY_CONTEXTMENU) { + const onLoad = BrowserTestUtils.waitForNewTab( + gBrowser, + destinationURL, + true + ); + const onLocationChanged = waitForLocationChanged(destinationURL); + + const onPopup = BrowserTestUtils.waitForEvent(document, "popupshown"); + await BrowserTestUtils.synthesizeMouseAtCenter( + linkSelector, + { type: "contextmenu" }, + gBrowser.selectedBrowser + ); + await onPopup; + const contextMenu = document.getElementById("contentAreaContextMenu"); + const openLinkMenuItem = contextMenu.querySelector( + "#context-openlinkintab" + ); + contextMenu.activateItem(openLinkMenuItem); + + const tab = await onLoad; + await onLocationChanged; + BrowserTestUtils.removeTab(tab); + } else if (openType === OPEN_TYPE.NEWWINDOW_BY_CONTEXTMENU) { + const onLoad = BrowserTestUtils.waitForNewWindow({ url: destinationURL }); + + const onPopup = BrowserTestUtils.waitForEvent(document, "popupshown"); + await BrowserTestUtils.synthesizeMouseAtCenter( + linkSelector, + { type: "contextmenu" }, + gBrowser.selectedBrowser + ); + await onPopup; + const contextMenu = document.getElementById("contentAreaContextMenu"); + const openLinkMenuItem = contextMenu.querySelector("#context-openlink"); + contextMenu.activateItem(openLinkMenuItem); + + const win = await onLoad; + await BrowserTestUtils.closeWindow(win); + } else if (openType === OPEN_TYPE.NEWWINDOW_BY_CONTEXTMENU_OF_TILE) { + const onLoad = BrowserTestUtils.waitForNewWindow({ url: destinationURL }); + + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [linkSelector], + async selector => { + const link = content.document.querySelector(selector); + const list = link.closest("li"); + const contextMenu = list.querySelector(".context-menu-button"); + contextMenu.click(); + const target = list.querySelector( + "[data-l10n-id=newtab-menu-open-new-window]" + ); + target.click(); + } + ); + + const win = await onLoad; + await BrowserTestUtils.closeWindow(win); + } + await promiseVisited; + + info("Check database for the destination."); + await assertDatabase({ targetURL: destinationURL, expected }); +} + +async function pin(link) { + // Setup test tile. + NewTabUtils.pinnedLinks.pin(link, 0); + await toggleTopsitesPref(); + await BrowserTestUtils.waitForCondition(() => { + const sites = AboutNewTab.getTopSites(); + return ( + sites?.[0]?.url === link.url && + sites[0].sponsored_tile_id === link.sponsored_tile_id + ); + }, "Waiting for top sites to be updated"); +} + +function unpin(link) { + NewTabUtils.pinnedLinks.unpin(link); +} + +add_setup(async function () { + await clearHistoryAndBookmarks(); + registerCleanupFunction(async () => { + await clearHistoryAndBookmarks(); + }); +}); + +add_task(async function basic() { + const SPONSORED_LINK = { + label: "test_label", + url: "https://example.com/", + sponsored_position: 1, + sponsored_tile_id: 12345, + sponsored_impression_url: "https://impression.example.com/", + sponsored_click_url: "https://click.example.com/", + }; + const NORMAL_LINK = { + label: "test_label", + url: "https://example.com/", + }; + const BOOKMARKS = [ + { + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: Services.io.newURI("https://example.com/"), + title: "test bookmark", + }, + ]; + + const testData = [ + { + description: "Sponsored tile", + link: SPONSORED_LINK, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + }, + }, + { + description: "Sponsored tile in new tab by click with key", + link: SPONSORED_LINK, + openType: OPEN_TYPE.NEWTAB_BY_CLICK, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + }, + }, + { + description: "Sponsored tile in new tab by middle click", + link: SPONSORED_LINK, + openType: OPEN_TYPE.NEWTAB_BY_MIDDLECLICK, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + }, + }, + { + description: "Sponsored tile in new tab by context menu", + link: SPONSORED_LINK, + openType: OPEN_TYPE.NEWTAB_BY_CONTEXTMENU, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + }, + }, + { + description: "Sponsored tile in new window by context menu", + link: SPONSORED_LINK, + openType: OPEN_TYPE.NEWWINDOW_BY_CONTEXTMENU, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + }, + }, + { + description: "Sponsored tile in new window by context menu of tile", + link: SPONSORED_LINK, + openType: OPEN_TYPE.NEWWINDOW_BY_CONTEXTMENU_OF_TILE, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + }, + }, + { + description: "Bookmarked result", + link: NORMAL_LINK, + bookmarks: BOOKMARKS, + expected: { + source: VISIT_SOURCE_BOOKMARKED, + frecency: FRECENCY.BOOKMARKED, + }, + }, + { + description: "Bookmarked result in new tab by click with key", + link: NORMAL_LINK, + openType: OPEN_TYPE.NEWTAB_BY_CLICK, + bookmarks: BOOKMARKS, + expected: { + source: VISIT_SOURCE_BOOKMARKED, + frecency: FRECENCY.BOOKMARKED, + }, + }, + { + description: "Bookmarked result in new tab by middle click", + link: NORMAL_LINK, + openType: OPEN_TYPE.NEWTAB_BY_MIDDLECLICK, + bookmarks: BOOKMARKS, + expected: { + source: VISIT_SOURCE_BOOKMARKED, + frecency: FRECENCY.MIDDLECLICK_BOOKMARKED, + }, + }, + { + description: "Bookmarked result in new tab by context menu", + link: NORMAL_LINK, + openType: OPEN_TYPE.NEWTAB_BY_CONTEXTMENU, + bookmarks: BOOKMARKS, + expected: { + source: VISIT_SOURCE_BOOKMARKED, + frecency: FRECENCY.MIDDLECLICK_BOOKMARKED, + }, + }, + { + description: "Bookmarked result in new window by context menu", + link: NORMAL_LINK, + openType: OPEN_TYPE.NEWWINDOW_BY_CONTEXTMENU, + bookmarks: BOOKMARKS, + expected: { + source: VISIT_SOURCE_BOOKMARKED, + frecency: FRECENCY.NEWWINDOW_BOOKMARKED, + }, + }, + { + description: "Bookmarked result in new window by context menu of tile", + link: NORMAL_LINK, + openType: OPEN_TYPE.NEWWINDOW_BY_CONTEXTMENU_OF_TILE, + bookmarks: BOOKMARKS, + expected: { + source: VISIT_SOURCE_BOOKMARKED, + frecency: FRECENCY.BOOKMARKED, + }, + }, + { + description: "Sponsored and bookmarked result", + link: SPONSORED_LINK, + bookmarks: BOOKMARKS, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.BOOKMARKED, + }, + }, + { + description: + "Sponsored and bookmarked result in new tab by click with key", + link: SPONSORED_LINK, + openType: OPEN_TYPE.NEWTAB_BY_CLICK, + bookmarks: BOOKMARKS, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.BOOKMARKED, + }, + }, + { + description: "Sponsored and bookmarked result in new tab by middle click", + link: SPONSORED_LINK, + openType: OPEN_TYPE.NEWTAB_BY_MIDDLECLICK, + bookmarks: BOOKMARKS, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.MIDDLECLICK_BOOKMARKED, + }, + }, + { + description: "Sponsored and bookmarked result in new tab by context menu", + link: SPONSORED_LINK, + openType: OPEN_TYPE.NEWTAB_BY_CONTEXTMENU, + bookmarks: BOOKMARKS, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.MIDDLECLICK_BOOKMARKED, + }, + }, + { + description: + "Sponsored and bookmarked result in new window by context menu", + link: SPONSORED_LINK, + openType: OPEN_TYPE.NEWWINDOW_BY_CONTEXTMENU, + bookmarks: BOOKMARKS, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.NEWWINDOW_BOOKMARKED, + }, + }, + { + description: + "Sponsored and bookmarked result in new window by context menu of tile", + link: SPONSORED_LINK, + openType: OPEN_TYPE.NEWWINDOW_BY_CONTEXTMENU_OF_TILE, + bookmarks: BOOKMARKS, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.BOOKMARKED, + }, + }, + { + description: "Organic tile", + link: NORMAL_LINK, + expected: { + source: VISIT_SOURCE_ORGANIC, + frecency: FRECENCY.TYPED, + }, + }, + { + description: "Organic tile in new tab by click with key", + link: NORMAL_LINK, + openType: OPEN_TYPE.NEWTAB_BY_CLICK, + expected: { + source: VISIT_SOURCE_ORGANIC, + frecency: FRECENCY.TYPED, + }, + }, + { + description: "Organic tile in new tab by middle click", + link: NORMAL_LINK, + openType: OPEN_TYPE.NEWTAB_BY_MIDDLECLICK, + expected: { + source: VISIT_SOURCE_ORGANIC, + frecency: FRECENCY.MIDDLECLICK_TYPED, + }, + }, + { + description: "Organic tile in new tab by context menu", + link: NORMAL_LINK, + openType: OPEN_TYPE.NEWTAB_BY_CONTEXTMENU, + expected: { + source: VISIT_SOURCE_ORGANIC, + frecency: FRECENCY.MIDDLECLICK_TYPED, + }, + }, + { + description: "Organic tile in new window by context menu", + link: NORMAL_LINK, + openType: OPEN_TYPE.NEWWINDOW_BY_CONTEXTMENU, + expected: { + source: VISIT_SOURCE_ORGANIC, + frecency: FRECENCY.NEWWINDOW_TYPED, + }, + }, + { + description: "Organic tile in new window by context menu of tile", + link: NORMAL_LINK, + openType: OPEN_TYPE.NEWWINDOW_BY_CONTEXTMENU_OF_TILE, + expected: { + source: VISIT_SOURCE_ORGANIC, + frecency: FRECENCY.TYPED, + }, + }, + ]; + + for (const { description, link, openType, bookmarks, expected } of testData) { + info(description); + + await BrowserTestUtils.withNewTab("about:home", async () => { + // Setup test tile. + await pin(link); + + for (const bookmark of bookmarks || []) { + await PlacesUtils.bookmarks.insert(bookmark); + } + + await openAndTest({ + linkSelector: ".top-site-button", + linkURL: link.url, + openType, + expected, + }); + + await clearHistoryAndBookmarks(); + + unpin(link); + }); + } +}); + +add_task(async function redirection() { + await BrowserTestUtils.withNewTab("about:home", async () => { + const redirectTo = "https://example.com/"; + const link = { + label: "test_label", + url: "https://example.com/browser/browser/components/newtab/test/browser/redirect_to.sjs?/", + sponsored_position: 1, + sponsored_tile_id: 12345, + sponsored_impression_url: "https://impression.example.com/", + sponsored_click_url: "https://click.example.com/", + }; + + // Setup test tile. + await pin(link); + + // Test with new tab. + await openAndTest({ + linkSelector: ".top-site-button", + linkURL: link.url, + redirectTo, + openType: OPEN_TYPE.NEWTAB_BY_CLICK, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + triggerURL: link.url, + }, + }); + + // Check for URL causes the redirection. + await assertDatabase({ + targetURL: link.url, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + }, + }); + await clearHistoryAndBookmarks(); + + // Test with same tab. + await openAndTest({ + linkSelector: ".top-site-button", + linkURL: link.url, + redirectTo, + openType: OPEN_TYPE.NEWTAB_BY_CLICK, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + triggerURL: link.url, + }, + }); + + // Check for URL causes the redirection. + await assertDatabase({ + targetURL: link.url, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + }, + }); + await clearHistoryAndBookmarks(); + unpin(link); + }); +}); + +add_task(async function inherit() { + const host = "https://example.com/"; + const sameBaseDomainHost = "https://www.example.com/"; + const path = "browser/browser/components/newtab/test/browser/"; + const firstURL = `${host}${path}annotation_first.html`; + const secondURL = `${host}${path}annotation_second.html`; + const thirdURL = `${sameBaseDomainHost}${path}annotation_third.html`; + const outsideURL = "https://example.org/"; + + await BrowserTestUtils.withNewTab("about:home", async () => { + const link = { + label: "first", + url: firstURL, + sponsored_position: 1, + sponsored_tile_id: 12345, + sponsored_impression_url: "https://impression.example.com/", + sponsored_click_url: "https://click.example.com/", + }; + + // Setup test tile. + await pin(link); + + info("Open the tile to show first page in same tab"); + await openAndTest({ + linkSelector: ".top-site-button", + linkURL: link.url, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + }, + }); + + info("Open link on first page to show second page in new window"); + await openAndTest({ + linkSelector: "a", + linkURL: secondURL, + openType: OPEN_TYPE.NEWWINDOW_BY_CONTEXTMENU, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + triggerURL: link.url, + }, + }); + await PlacesTestUtils.clearHistoryVisits(); + + info( + "Open link on first page to show second page in new tab by click with key" + ); + await openAndTest({ + linkSelector: "a", + linkURL: secondURL, + openType: OPEN_TYPE.NEWTAB_BY_CLICK, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + triggerURL: link.url, + }, + }); + await PlacesTestUtils.clearHistoryVisits(); + + info( + "Open link on first page to show second page in new tab by middle click" + ); + await openAndTest({ + linkSelector: "a", + linkURL: secondURL, + openType: OPEN_TYPE.NEWTAB_BY_MIDDLECLICK, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + triggerURL: link.url, + }, + }); + await PlacesTestUtils.clearHistoryVisits(); + + info("Open link on first page to show second page in same tab"); + await openAndTest({ + linkSelector: "a", + linkURL: secondURL, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + triggerURL: link.url, + }, + }); + + info("Open link on first page to show second page in new window"); + await openAndTest({ + linkSelector: "a", + linkURL: thirdURL, + openType: OPEN_TYPE.NEWWINDOW_BY_CONTEXTMENU, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + triggerURL: link.url, + }, + }); + await PlacesTestUtils.clearHistoryVisits(); + + info( + "Open link on second page to show third page in new tab by context menu" + ); + await openAndTest({ + linkSelector: "a", + linkURL: thirdURL, + openType: OPEN_TYPE.NEWTAB_BY_CONTEXTMENU, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + triggerURL: link.url, + }, + }); + await PlacesTestUtils.clearHistoryVisits(); + + info( + "Open link on second page to show third page in new tab by middle click" + ); + await openAndTest({ + linkSelector: "a", + linkURL: thirdURL, + openType: OPEN_TYPE.NEWTAB_BY_MIDDLECLICK, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + triggerURL: link.url, + }, + }); + await PlacesTestUtils.clearHistoryVisits(); + + info("Open link on second page to show third page in same tab"); + await openAndTest({ + linkSelector: "a", + linkURL: thirdURL, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + triggerURL: link.url, + }, + }); + + info("Open link on third page to show outside domain page in same tab"); + await openAndTest({ + linkSelector: "a", + linkURL: outsideURL, + expected: { + source: VISIT_SOURCE_ORGANIC, + frecency: FRECENCY.VISITED, + }, + }); + + info("Visit URL that has the same domain as sponsored link from URL bar"); + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + host + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: host, + waitForFocus: SimpleTest.waitForFocus, + }); + let promiseVisited = waitForVisitNotification(host); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + await promiseVisited; + + await assertDatabase({ + targetURL: host, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + triggerURL: link.url, + }, + }); + + unpin(link); + await clearHistoryAndBookmarks(); + }); +}); + +add_task(async function timeout() { + const base = + "https://example.com/browser/browser/components/newtab/test/browser"; + const firstURL = `${base}/annotation_first.html`; + const secondURL = `${base}/annotation_second.html`; + + await BrowserTestUtils.withNewTab("about:home", async () => { + const link = { + label: "test", + url: firstURL, + sponsored_position: 1, + sponsored_tile_id: 12345, + sponsored_impression_url: "https://impression.example.com/", + sponsored_click_url: "https://click.example.com/", + }; + + // Setup a test tile. + await pin(link); + + info("Open the tile"); + await openAndTest({ + linkSelector: ".top-site-button", + linkURL: link.url, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + }, + }); + + info("Set timeout second"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.places.sponsoredSession.timeoutSecs", 1]], + }); + + info("Wait 1 sec"); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 1000)); + + info("Open link on first page to show second page in new window"); + await openAndTest({ + linkSelector: "a", + linkURL: secondURL, + openType: OPEN_TYPE.NEWWINDOW_BY_CONTEXTMENU, + expected: { + source: VISIT_SOURCE_ORGANIC, + frecency: FRECENCY.VISITED, + }, + }); + await PlacesTestUtils.clearHistoryVisits(); + + info( + "Open link on first page to show second page in new tab by click with key" + ); + await openAndTest({ + linkSelector: "a", + linkURL: secondURL, + openType: OPEN_TYPE.NEWTAB_BY_CLICK, + expected: { + source: VISIT_SOURCE_ORGANIC, + frecency: FRECENCY.VISITED, + }, + }); + await PlacesTestUtils.clearHistoryVisits(); + + info( + "Open link on first page to show second page in new tab by middle click" + ); + await openAndTest({ + linkSelector: "a", + linkURL: secondURL, + openType: OPEN_TYPE.NEWTAB_BY_MIDDLECLICK, + expected: { + source: VISIT_SOURCE_ORGANIC, + frecency: FRECENCY.VISITED, + }, + }); + await PlacesTestUtils.clearHistoryVisits(); + + info("Open link on first page to show second page"); + await openAndTest({ + linkSelector: "a", + linkURL: secondURL, + expected: { + source: VISIT_SOURCE_ORGANIC, + frecency: FRECENCY.VISITED, + }, + }); + + unpin(link); + await clearHistoryAndBookmarks(); + }); +}); + +add_task(async function fixup() { + await BrowserTestUtils.withNewTab("about:home", async () => { + const destinationURL = "https://example.com/?a"; + const link = { + label: "test", + url: "https://example.com?a", + sponsored_position: 1, + sponsored_tile_id: 12345, + sponsored_impression_url: "https://impression.example.com/", + sponsored_click_url: "https://click.example.com/", + }; + + info("Setup pin"); + await pin(link); + + info("Click sponsored tile"); + let promiseVisited = waitForVisitNotification(destinationURL); + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + destinationURL + ); + const onLocationChanged = waitForLocationChanged(destinationURL); + await BrowserTestUtils.synthesizeMouseAtCenter( + ".top-site-button", + {}, + gBrowser.selectedBrowser + ); + await onLoad; + await onLocationChanged; + await promiseVisited; + + info("Check the DB"); + await assertDatabase({ + targetURL: destinationURL, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + }, + }); + + info("Clean up"); + unpin(link); + await clearHistoryAndBookmarks(); + }); +}); + +add_task(async function noTriggeringURL() { + await BrowserTestUtils.withNewTab("about:home", async browser => { + Services.telemetry.clearScalars(); + + const dummyTriggeringSponsoredURL = + "https://example.com/dummyTriggeringSponsoredURL"; + const targetURL = "https://example.com/"; + + info("Setup dummy triggering sponsored URL"); + browser.setAttribute("triggeringSponsoredURL", dummyTriggeringSponsoredURL); + browser.setAttribute("triggeringSponsoredURLVisitTimeMS", Date.now()); + + info("Open URL whose host is the same as dummy triggering sponsored URL"); + let promiseVisited = waitForVisitNotification(targetURL); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: targetURL, + waitForFocus: SimpleTest.waitForFocus, + }); + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + targetURL + ); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + await promiseVisited; + + info("Check DB"); + await assertDatabase({ + targetURL, + expected: { + source: VISIT_SOURCE_SPONSORED, + frecency: FRECENCY.SPONSORED, + }, + }); + + info("Check telemetry"); + const scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar( + scalars, + "places.sponsored_visit_no_triggering_url", + 1 + ); + + await clearHistoryAndBookmarks(); + }); +}); diff --git a/browser/components/newtab/test/browser/browser_topsites_contextMenu_options.js b/browser/components/newtab/test/browser/browser_topsites_contextMenu_options.js new file mode 100644 index 0000000000..c744e8ee01 --- /dev/null +++ b/browser/components/newtab/test/browser/browser_topsites_contextMenu_options.js @@ -0,0 +1,126 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +test_newtab({ + async before() { + // Some reason test-linux1804-64-qr/debug can end up with example.com, so + // clear history so we only have the expected default top sites. + await clearHistoryAndBookmarks(); + await setDefaultTopSites(); + }, + // Test verifies the menu options for a default top site. + test: async function defaultTopSites_menuOptions() { + const siteSelector = ".top-site-outer:not(.search-shortcut, .placeholder)"; + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(siteSelector), + "Topsite tippytop icon not found" + ); + + const contextMenuItems = await content.openContextMenuAndGetOptions( + siteSelector + ); + + Assert.equal(contextMenuItems.length, 5, "Number of options is correct"); + + const expectedItemsText = [ + "Pin", + "Edit", + "Open in a New Window", + "Open in a New Private Window", + "Dismiss", + ]; + + for (let i = 0; i < contextMenuItems.length; i++) { + await ContentTaskUtils.waitForCondition( + () => contextMenuItems[i].textContent === expectedItemsText[i], + "Name option is correct" + ); + } + }, +}); + +test_newtab({ + before: setDefaultTopSites, + // Test verifies that the next top site in queue replaces a dismissed top site. + test: async function defaultTopSites_dismiss() { + const siteSelector = ".top-site-outer:not(.search-shortcut, .placeholder)"; + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(siteSelector), + "Topsite tippytop icon not found" + ); + + // Don't count search topsites + const defaultTopSitesNumber = + content.document.querySelectorAll(siteSelector).length; + Assert.equal(defaultTopSitesNumber, 5, "5 top sites are loaded by default"); + + // Skip the search topsites select the second default topsite + const secondTopSite = content.document + .querySelectorAll(siteSelector)[1] + .getAttribute("href"); + + const contextMenuItems = await content.openContextMenuAndGetOptions( + siteSelector + ); + await ContentTaskUtils.waitForCondition( + () => contextMenuItems[4].textContent === "Dismiss", + "'Dismiss' is the 5th item in the context menu list" + ); + + contextMenuItems[4].querySelector("button").click(); + + // Wait for the topsite to be dismissed and the second one to replace it + await ContentTaskUtils.waitForCondition( + () => + content.document.querySelector(siteSelector).getAttribute("href") === + secondTopSite, + "First default topsite was dismissed" + ); + + await ContentTaskUtils.waitForCondition( + () => content.document.querySelectorAll(siteSelector).length === 4, + "4 top sites are displayed after one of them is dismissed" + ); + }, + async after() { + await new Promise(resolve => NewTabUtils.undoAll(resolve)); + }, +}); + +test_newtab({ + before: setDefaultTopSites, + test: async function searchTopSites_dismiss() { + const siteSelector = ".search-shortcut"; + await ContentTaskUtils.waitForCondition( + () => content.document.querySelectorAll(siteSelector).length === 1, + "1 search topsites is loaded by default" + ); + + const contextMenuItems = await content.openContextMenuAndGetOptions( + siteSelector + ); + is( + contextMenuItems.length, + 2, + "Search TopSites should only have Unpin and Dismiss" + ); + + // Unpin + contextMenuItems[0].querySelector("button").click(); + + await ContentTaskUtils.waitForCondition( + () => content.document.querySelectorAll(siteSelector).length === 1, + "1 search topsite displayed after we unpin the other one" + ); + }, + after: () => { + // Required for multiple test runs in the same browser, pref is used to + // prevent pinning the same search topsite twice + Services.prefs.clearUserPref( + "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts.havePinned" + ); + }, +}); diff --git a/browser/components/newtab/test/browser/browser_topsites_section.js b/browser/components/newtab/test/browser/browser_topsites_section.js new file mode 100644 index 0000000000..df569628d1 --- /dev/null +++ b/browser/components/newtab/test/browser/browser_topsites_section.js @@ -0,0 +1,304 @@ +"use strict"; + +// Check TopSites edit modal and overlay show up. +test_newtab({ + before: setTestTopSites, + // it should be able to click the topsites add button to reveal the add top site modal and overlay. + test: async function topsites_edit() { + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(".top-sites .context-menu-button"), + "Should find a visible topsite context menu button [topsites_edit]" + ); + + // Open the section context menu. + content.document.querySelector(".top-sites .context-menu-button").click(); + + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(".top-sites .context-menu"), + "Should find a visible topsite context menu [topsites_edit]" + ); + + const topsitesAddBtn = content.document.querySelector( + ".top-sites li:nth-child(2) button" + ); + topsitesAddBtn.click(); + + let found = content.document.querySelector(".topsite-form"); + ok(found && !found.hidden, "Should find a visible topsite form"); + + found = content.document.querySelector(".modalOverlayOuter"); + ok(found && !found.hidden, "Should find a visible overlay"); + }, +}); + +// Test pin/unpin context menu options. +test_newtab({ + before: setDefaultTopSites, + // it should pin the website when we click the first option of the topsite context menu. + test: async function topsites_pin_unpin() { + const siteSelector = ".top-site-outer:not(.search-shortcut, .placeholder)"; + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(siteSelector), + "Topsite tippytop icon not found" + ); + // There are only topsites on the page, the selector with find the first topsite menu button. + let topsiteEl = content.document.querySelector(siteSelector); + let topsiteContextBtn = topsiteEl.querySelector(".context-menu-button"); + topsiteContextBtn.click(); + + await ContentTaskUtils.waitForCondition( + () => topsiteEl.querySelector(".top-sites-list .context-menu"), + "No context menu found" + ); + + let contextMenu = topsiteEl.querySelector(".top-sites-list .context-menu"); + ok(contextMenu, "Should find a topsite context menu"); + + const pinUnpinTopsiteBtn = contextMenu.querySelector( + ".top-sites .context-menu-item button" + ); + // Pin the topsite. + pinUnpinTopsiteBtn.click(); + + // Need to wait for pin action. + await ContentTaskUtils.waitForCondition( + () => topsiteEl.querySelector(".icon-pin-small"), + "No pinned icon found" + ); + + let pinnedIcon = topsiteEl.querySelectorAll(".icon-pin-small").length; + is(pinnedIcon, 1, "should find 1 pinned topsite"); + + // Unpin the topsite. + topsiteContextBtn = topsiteEl.querySelector(".context-menu-button"); + ok(topsiteContextBtn, "Should find a context menu button"); + topsiteContextBtn.click(); + topsiteEl.querySelector(".context-menu-item button").click(); + + // Need to wait for unpin action. + await ContentTaskUtils.waitForCondition( + () => !topsiteEl.querySelector(".icon-pin-small"), + "Topsite should be unpinned" + ); + }, +}); + +// Check Topsites add +test_newtab({ + before: setTestTopSites, + // it should be able to click the topsites edit button to reveal the edit topsites modal and overlay. + test: async function topsites_add() { + let nativeInputValueSetter = Object.getOwnPropertyDescriptor( + content.window.HTMLInputElement.prototype, + "value" + ).set; + let event = new content.Event("input", { bubbles: true }); + + // Wait for context menu button to load + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(".top-sites .context-menu-button"), + "Should find a visible topsite context menu button [topsites_add]" + ); + + content.document.querySelector(".top-sites .context-menu-button").click(); + + // Wait for context menu to load + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(".top-sites .context-menu"), + "Should find a visible topsite context menu [topsites_add]" + ); + + // Find topsites edit button + const topsitesAddBtn = content.document.querySelector( + ".top-sites li:nth-child(2) button" + ); + + topsitesAddBtn.click(); + + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(".modalOverlayOuter"), + "No overlay found" + ); + + let found = content.document.querySelector(".modalOverlayOuter"); + ok(found && !found.hidden, "Should find a visible overlay"); + + // Write field title + let fieldTitle = content.document.querySelector(".field input"); + ok(fieldTitle && !fieldTitle.hidden, "Should find field title input"); + + nativeInputValueSetter.call(fieldTitle, "Bugzilla"); + fieldTitle.dispatchEvent(event); + is(fieldTitle.value, "Bugzilla", "The field title should match"); + + // Write field url + let fieldURL = content.document.querySelector(".field.url input"); + ok(fieldURL && !fieldURL.hidden, "Should find field url input"); + + nativeInputValueSetter.call(fieldURL, "https://bugzilla.mozilla.org"); + fieldURL.dispatchEvent(event); + is( + fieldURL.value, + "https://bugzilla.mozilla.org", + "The field url should match" + ); + + // Click the "Add" button + let addBtn = content.document.querySelector(".done"); + addBtn.click(); + + // Wait for Topsite to be populated + await ContentTaskUtils.waitForCondition( + () => + content.document.querySelector("[href='https://bugzilla.mozilla.org']"), + "No Topsite found" + ); + + // Remove topsite after test is complete + let topsiteContextBtn = content.document.querySelector( + ".top-sites-list li:nth-child(1) .context-menu-button" + ); + topsiteContextBtn.click(); + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(".top-sites-list .context-menu"), + "No context menu found" + ); + + const dismissBtn = content.document.querySelector( + ".top-sites li:nth-child(7) button" + ); + dismissBtn.click(); + + // Wait for Topsite to be removed + await ContentTaskUtils.waitForCondition( + () => + !content.document.querySelector( + "[href='https://bugzilla.mozilla.org']" + ), + "Topsite not removed" + ); + }, +}); + +test_newtab({ + before: setDefaultTopSites, + test: async function test_search_topsite_keyword() { + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(".search-shortcut .title.pinned"), + "Wait for pinned search topsites" + ); + + const searchTopSites = content.document.querySelectorAll(".title.pinned"); + Assert.greaterOrEqual( + searchTopSites.length, + 1, + "There should be at least 1 search topsites" + ); + + searchTopSites[0].click(); + + return searchTopSites[0].innerText.trim(); + }, + async after(searchTopSiteTag) { + ok( + gURLBar.focused, + "We clicked a search topsite the focus should be in location bar" + ); + let engine = await Services.search.getEngineByAlias(searchTopSiteTag); + + // We don't use UrlbarTestUtils.assertSearchMode here since the newtab + // testing scope doesn't integrate well with UrlbarTestUtils. + Assert.deepEqual( + gURLBar.searchMode, + { + engineName: engine.name, + entry: "topsites_newtab", + isPreview: false, + isGeneralPurposeEngine: false, + }, + "The Urlbar is in search mode." + ); + ok( + gURLBar.hasAttribute("searchmode"), + "The Urlbar has the searchmode attribute." + ); + }, +}); + +// test_newtab is not used here as this test requires two steps into the +// content process with chrome process activity in-between. +add_task(async function test_search_topsite_remove_engine() { + // Open about:newtab without using the default load listener + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:newtab", + false + ); + + // Specially wait for potentially preloaded browsers + let browser = tab.linkedBrowser; + await waitForPreloaded(browser); + + // Add shared helpers to the content process + SpecialPowers.spawn(browser, [], addContentHelpers); + + // Wait for React to render something + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => content.document.getElementById("root").children.length + ), + "Should render activity stream content" + ); + + await setDefaultTopSites(); + + let [topSiteAlias, numTopSites] = await SpecialPowers.spawn( + browser, + [], + async () => { + await ContentTaskUtils.waitForCondition( + () => content.document.querySelector(".search-shortcut .title.pinned"), + "Wait for pinned search topsites" + ); + + const searchTopSites = content.document.querySelectorAll(".title.pinned"); + Assert.greaterOrEqual( + searchTopSites.length, + 1, + "There should be at least one topsite" + ); + return [searchTopSites[0].innerText.trim(), searchTopSites.length]; + } + ); + + await Services.search.removeEngine( + await Services.search.getEngineByAlias(topSiteAlias) + ); + + registerCleanupFunction(() => { + Services.search.restoreDefaultEngines(); + }); + + await SpecialPowers.spawn( + browser, + [numTopSites], + async originalNumTopSites => { + await ContentTaskUtils.waitForCondition( + () => !content.document.querySelector(".search-shortcut .title.pinned"), + "Wait for pinned search topsites" + ); + + const searchTopSites = content.document.querySelectorAll(".title.pinned"); + is( + searchTopSites.length, + originalNumTopSites - 1, + "There should be one less search topsites" + ); + } + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/newtab/test/browser/browser_trigger_messagesLoaded.js b/browser/components/newtab/test/browser/browser_trigger_messagesLoaded.js new file mode 100644 index 0000000000..a076f9178e --- /dev/null +++ b/browser/components/newtab/test/browser/browser_trigger_messagesLoaded.js @@ -0,0 +1,153 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { ASRouter } = ChromeUtils.importESModule( + "resource:///modules/asrouter/ASRouter.sys.mjs" +); +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); +const { RemoteSettingsExperimentLoader } = ChromeUtils.importESModule( + "resource://nimbus/lib/RemoteSettingsExperimentLoader.sys.mjs" +); +const { ExperimentAPI } = ChromeUtils.importESModule( + "resource://nimbus/ExperimentAPI.sys.mjs" +); +const { ExperimentFakes, ExperimentTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/NimbusTestUtils.sys.mjs" +); + +const client = RemoteSettings("nimbus-desktop-experiments"); + +const TEST_MESSAGE_CONTENT = { + id: "ON_LOAD_TEST_MESSAGE", + template: "cfr_doorhanger", + content: { + bucket_id: "ON_LOAD_TEST_MESSAGE", + anchor_id: "PanelUI-menu-button", + layout: "icon_and_message", + icon: "chrome://activity-stream/content/data/content/assets/glyph-webextension-16.svg", + icon_dark_theme: + "chrome://activity-stream/content/data/content/assets/glyph-webextension-16.svg", + icon_class: "cfr-doorhanger-small-icon", + heading_text: "Heading", + text: "Text", + buttons: { + primary: { + label: { value: "Primary CTA", attributes: { accesskey: "P" } }, + action: { navigate: true }, + }, + secondary: [ + { + label: { value: "Secondary CTA", attributes: { accesskey: "S" } }, + action: { type: "CANCEL" }, + }, + ], + }, + skip_address_bar_notifier: true, + }, + targeting: "true", + trigger: { id: "messagesLoaded" }, +}; + +add_task(async function test_messagesLoaded_reach_experiment() { + const sandbox = sinon.createSandbox(); + const sendTriggerSpy = sandbox.spy(ASRouter, "sendTriggerMessage"); + const routeSpy = sandbox.spy(ASRouter, "routeCFRMessage"); + const reachSpy = sandbox.spy(ASRouter, "_recordReachEvent"); + const triggerMatch = sandbox.match({ id: "messagesLoaded" }); + const featureId = "cfr"; + const recipe = ExperimentFakes.recipe( + `messages_loaded_test_${Services.uuid + .generateUUID() + .toString() + .slice(1, -1)}`, + { + id: `messages-loaded-test`, + bucketConfig: { + count: 100, + start: 0, + total: 100, + namespace: "mochitest", + randomizationUnit: "normandy_id", + }, + branches: [ + { + slug: "control", + ratio: 1, + features: [ + { + featureId, + value: { ...TEST_MESSAGE_CONTENT, id: "messages-loaded-test-1" }, + }, + ], + }, + { + slug: "treatment", + ratio: 1, + features: [ + { + featureId, + value: { ...TEST_MESSAGE_CONTENT, id: "messages-loaded-test-2" }, + }, + ], + }, + ], + } + ); + Assert.ok( + await ExperimentTestUtils.validateExperiment(recipe), + "Valid recipe" + ); + + await client.db.importChanges({}, Date.now(), [recipe], { clear: true }); + await SpecialPowers.pushPrefEnv({ + set: [ + ["app.shield.optoutstudies.enabled", true], + ["datareporting.healthreport.uploadEnabled", true], + [ + "browser.newtabpage.activity-stream.asrouter.providers.messaging-experiments", + `{"id":"messaging-experiments","enabled":true,"type":"remote-experiments","updateCycleInMs":0}`, + ], + ], + }); + await RemoteSettingsExperimentLoader.updateRecipes(); + await BrowserTestUtils.waitForCondition( + () => ExperimentAPI.getExperiment({ featureId }), + "ExperimentAPI should return an experiment" + ); + + await ASRouter._updateMessageProviders(); + await ASRouter.loadMessagesFromAllProviders(); + + const filterFn = m => + ["messages-loaded-test-1", "messages-loaded-test-2"].includes(m?.id); + await BrowserTestUtils.waitForCondition( + () => ASRouter.state.messages.filter(filterFn).length > 1, + "Should load the test messages" + ); + Assert.ok(sendTriggerSpy.calledWith(triggerMatch, true), "Trigger fired"); + Assert.ok( + routeSpy.calledWith( + sandbox.match(filterFn), + gBrowser.selectedBrowser, + triggerMatch + ), + "Trigger routed to the correct message" + ); + Assert.ok( + reachSpy.calledWith(sandbox.match(filterFn)), + "Trigger recorded a reach event" + ); + Assert.ok( + ASRouter.state.messages.find(m => filterFn(m) && m.forReachEvent) + ?.forReachEvent.sent, + "Reach message will not be sent again" + ); + + sandbox.restore(); + await client.db.clear(); + await SpecialPowers.popPrefEnv(); + await ASRouter._updateMessageProviders(); +}); diff --git a/browser/components/newtab/test/browser/file_pdf.PDF b/browser/components/newtab/test/browser/file_pdf.PDF new file mode 100644 index 0000000000..593558f9a4 --- /dev/null +++ b/browser/components/newtab/test/browser/file_pdf.PDF @@ -0,0 +1,12 @@ +%PDF-1.0
+1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 3 3]>>endobj
+xref
+0 4
+0000000000 65535 f
+0000000010 00000 n
+0000000053 00000 n
+0000000102 00000 n
+trailer<</Size 4/Root 1 0 R>>
+startxref
+149
+%EOF
\ No newline at end of file diff --git a/browser/components/newtab/test/browser/head.js b/browser/components/newtab/test/browser/head.js new file mode 100644 index 0000000000..1dbae8af02 --- /dev/null +++ b/browser/components/newtab/test/browser/head.js @@ -0,0 +1,244 @@ +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs", + + DiscoveryStreamFeed: + "resource://activity-stream/lib/DiscoveryStreamFeed.sys.mjs", + + FeatureCallout: "resource:///modules/asrouter/FeatureCallout.sys.mjs", + + FeatureCalloutBroker: + "resource:///modules/asrouter/FeatureCalloutBroker.sys.mjs", + + FeatureCalloutMessages: + "resource:///modules/asrouter/FeatureCalloutMessages.sys.mjs", + + ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs", + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", + QueryCache: "resource:///modules/asrouter/ASRouterTargeting.sys.mjs", +}); + +// We import sinon here to make it available across all mochitest test files +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +function popPrefs() { + return SpecialPowers.popPrefEnv(); +} +function pushPrefs(...prefs) { + return SpecialPowers.pushPrefEnv({ set: prefs }); +} + +// Toggle the feed off and on as a workaround to read the new prefs. +async function toggleTopsitesPref() { + await pushPrefs([ + "browser.newtabpage.activity-stream.feeds.system.topsites", + false, + ]); + await pushPrefs([ + "browser.newtabpage.activity-stream.feeds.system.topsites", + true, + ]); +} + +async function setDefaultTopSites() { + // The pref for TopSites is empty by default. + await pushPrefs([ + "browser.newtabpage.activity-stream.default.sites", + "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/", + ]); + await toggleTopsitesPref(); + await pushPrefs([ + "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts", + true, + ]); +} + +async function setTestTopSites() { + await pushPrefs([ + "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts", + false, + ]); + // The pref for TopSites is empty by default. + // Using a topsite with example.com allows us to open the topsite without a network request. + await pushPrefs([ + "browser.newtabpage.activity-stream.default.sites", + "https://example.com/", + ]); + await toggleTopsitesPref(); +} + +async function clearHistoryAndBookmarks() { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + QueryCache.expireAll(); +} + +/** + * Helper to wait for potentially preloaded browsers to "load" where a preloaded + * page has already loaded and won't trigger "load", and a "load"ed page might + * not necessarily have had all its javascript/render logic executed. + */ +async function waitForPreloaded(browser) { + let readyState = await ContentTask.spawn( + browser, + null, + () => content.document.readyState + ); + if (readyState !== "complete") { + await BrowserTestUtils.browserLoaded(browser); + } +} + +/** + * Helper to force the HighlightsFeed to update. + */ +function refreshHighlightsFeed() { + // Toggling the pref will clear the feed cache and force a places query. + Services.prefs.setBoolPref( + "browser.newtabpage.activity-stream.feeds.section.highlights", + false + ); + Services.prefs.setBoolPref( + "browser.newtabpage.activity-stream.feeds.section.highlights", + true + ); +} + +/** + * Helper to populate the Highlights section with bookmark cards. + * @param count Number of items to add. + */ +async function addHighlightsBookmarks(count) { + const bookmarks = new Array(count).fill(null).map((entry, i) => ({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "foo", + url: `https://mozilla${i}.com/nowNew`, + })); + + for (let placeInfo of bookmarks) { + await PlacesUtils.bookmarks.insert(placeInfo); + // Bookmarks need at least one visit to show up as highlights. + await PlacesTestUtils.addVisits(placeInfo.url); + } + + // Force HighlightsFeed to make a request for the new items. + refreshHighlightsFeed(); +} + +/** + * Helper to add various helpers to the content process by injecting variables + * and functions to the `content` global. + */ +function addContentHelpers() { + const { document } = content; + Object.assign(content, { + /** + * Click the context menu button for an item and get its options list. + * + * @param selector {String} Selector to get an item (e.g., top site, card) + * @return {Array} The nodes for the options. + */ + async openContextMenuAndGetOptions(selector) { + const item = document.querySelector(selector); + const contextButton = item.querySelector(".context-menu-button"); + contextButton.click(); + // Gives fluent-dom the time to render strings + await new Promise(r => content.requestAnimationFrame(r)); + + const contextMenu = item.querySelector(".context-menu"); + const contextMenuList = contextMenu.querySelector(".context-menu-list"); + return [...contextMenuList.getElementsByClassName("context-menu-item")]; + }, + }); +} + +/** + * Helper to run Activity Stream about:newtab test tasks in content. + * + * @param testInfo {Function|Object} + * {Function} This parameter will be used as if the function were called with + * an Object with this parameter as "test" key's value. + * {Object} The following keys are expected: + * before {Function} Optional. Runs before and returns an arg for "test" + * test {Function} The test to run in the about:newtab content task taking + * an arg from "before" and returns a result to "after" + * after {Function} Optional. Runs after and with the result of "test" + * @param browserURL {optional String} + * {String} This parameter is used to explicitly specify URL opened in new tab + */ +function test_newtab(testInfo, browserURL = "about:newtab") { + // Extract any test parts or default to just the single content task + let { before, test: contentTask, after } = testInfo; + if (!before) { + before = () => ({}); + } + if (!contentTask) { + contentTask = testInfo; + } + if (!after) { + after = () => {}; + } + + // Helper to push prefs for just this test and pop them when done + let needPopPrefs = false; + let scopedPushPrefs = async (...args) => { + needPopPrefs = true; + await pushPrefs(...args); + }; + let scopedPopPrefs = async () => { + if (needPopPrefs) { + await popPrefs(); + } + }; + + // Make the test task with optional before/after and content task to run in a + // new tab that opens and closes. + let testTask = async () => { + // Open about:newtab without using the default load listener + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + browserURL, + false + ); + + // Specially wait for potentially preloaded browsers + let browser = tab.linkedBrowser; + await waitForPreloaded(browser); + + // Add shared helpers to the content process + SpecialPowers.spawn(browser, [], addContentHelpers); + + // Wait for React to render something + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => content.document.getElementById("root").children.length + ), + "Should render activity stream content" + ); + + // Chain together before -> contentTask -> after data passing + try { + let contentArg = await before({ pushPrefs: scopedPushPrefs, tab }); + let contentResult = await SpecialPowers.spawn( + browser, + [contentArg], + contentTask + ); + await after(contentResult); + } finally { + // Clean up for next tests + BrowserTestUtils.removeTab(tab); + await scopedPopPrefs(); + } + }; + + // Copy the name of the content task to identify the test + Object.defineProperty(testTask, "name", { value: contentTask.name }); + add_task(testTask); +} diff --git a/browser/components/newtab/test/browser/red_page.html b/browser/components/newtab/test/browser/red_page.html new file mode 100644 index 0000000000..733a1f0d4a --- /dev/null +++ b/browser/components/newtab/test/browser/red_page.html @@ -0,0 +1,6 @@ +<html> + <head> + <meta charset="utf-8"> + </head> + <body style="background-color: red" /> +</html> diff --git a/browser/components/newtab/test/browser/redirect_to.sjs b/browser/components/newtab/test/browser/redirect_to.sjs new file mode 100644 index 0000000000..b52ebdc63e --- /dev/null +++ b/browser/components/newtab/test/browser/redirect_to.sjs @@ -0,0 +1,9 @@ +"use strict"; + +function handleRequest(request, response) { + // redirect_to.sjs?ctxmenu-image.png + // redirects to : ctxmenu-image.png + const redirectUrl = request.queryString; + response.setStatusLine(request.httpVersion, "302", "Found"); + response.setHeader("Location", redirectUrl, false); +} diff --git a/browser/components/newtab/test/browser/topstories.json b/browser/components/newtab/test/browser/topstories.json new file mode 100644 index 0000000000..0c27dfe1a2 --- /dev/null +++ b/browser/components/newtab/test/browser/topstories.json @@ -0,0 +1,11 @@ +{ + "data": [ + { + "tileId": 53093, + "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAA/UlEQVR4nO3RMQ0AMAzAsPIn3d5DsBw2gkiZJWV+B/AyJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQGENiDIkxJMaQmAP4K6zWNUjE4wAAAABJRU5ErkJggg==", + "publisher": "bbc", + "title": "Why vegan junk food may be even worse for your health", + "excerpt": "While we might switch to a plant-based diet with the best intentions, the unseen risks of vegan fast foods might not show up for years." + } + ] +} |