summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/test/browser
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/test/browser')
-rw-r--r--browser/components/newtab/test/browser/abouthomecache/browser.toml52
-rw-r--r--browser/components/newtab/test/browser/abouthomecache/browser_basic_endtoend.js22
-rw-r--r--browser/components/newtab/test/browser/abouthomecache/browser_bump_version.js35
-rw-r--r--browser/components/newtab/test/browser/abouthomecache/browser_disabled.js97
-rw-r--r--browser/components/newtab/test/browser/abouthomecache/browser_experiments_api_control.js63
-rw-r--r--browser/components/newtab/test/browser/abouthomecache/browser_locale_change.js30
-rw-r--r--browser/components/newtab/test/browser/abouthomecache/browser_no_cache.js27
-rw-r--r--browser/components/newtab/test/browser/abouthomecache/browser_no_cache_on_SessionStartup_restore.js37
-rw-r--r--browser/components/newtab/test/browser/abouthomecache/browser_no_startup_actions.js83
-rw-r--r--browser/components/newtab/test/browser/abouthomecache/browser_overwrite_cache.js38
-rw-r--r--browser/components/newtab/test/browser/abouthomecache/browser_process_crash.js82
-rw-r--r--browser/components/newtab/test/browser/abouthomecache/browser_same_consumer.js52
-rw-r--r--browser/components/newtab/test/browser/abouthomecache/browser_sanitize.js54
-rw-r--r--browser/components/newtab/test/browser/abouthomecache/browser_shutdown_timeout.js45
-rw-r--r--browser/components/newtab/test/browser/abouthomecache/head.js365
-rw-r--r--browser/components/newtab/test/browser/annotation_first.html2
-rw-r--r--browser/components/newtab/test/browser/annotation_second.html2
-rw-r--r--browser/components/newtab/test/browser/annotation_third.html2
-rw-r--r--browser/components/newtab/test/browser/blue_page.html6
-rw-r--r--browser/components/newtab/test/browser/browser.toml81
-rw-r--r--browser/components/newtab/test/browser/browser_as_load_location.js44
-rw-r--r--browser/components/newtab/test/browser/browser_as_render.js83
-rw-r--r--browser/components/newtab/test/browser/browser_context_menu_item.js18
-rw-r--r--browser/components/newtab/test/browser/browser_customize_menu_content.js219
-rw-r--r--browser/components/newtab/test/browser/browser_customize_menu_render.js28
-rw-r--r--browser/components/newtab/test/browser/browser_discovery_card.js49
-rw-r--r--browser/components/newtab/test/browser/browser_discovery_render.js31
-rw-r--r--browser/components/newtab/test/browser/browser_enabled_newtabpage.js33
-rw-r--r--browser/components/newtab/test/browser/browser_foxdoodle_set_default.js69
-rw-r--r--browser/components/newtab/test/browser/browser_getScreenshots.js88
-rw-r--r--browser/components/newtab/test/browser/browser_highlights_section.js96
-rw-r--r--browser/components/newtab/test/browser/browser_multistage_spotlight.js90
-rw-r--r--browser/components/newtab/test/browser/browser_multistage_spotlight_telemetry.js141
-rw-r--r--browser/components/newtab/test/browser/browser_newtab_glean.js28
-rw-r--r--browser/components/newtab/test/browser/browser_newtab_header.js76
-rw-r--r--browser/components/newtab/test/browser/browser_newtab_last_LinkMenu.js153
-rw-r--r--browser/components/newtab/test/browser/browser_newtab_overrides.js134
-rw-r--r--browser/components/newtab/test/browser/browser_newtab_ping.js216
-rw-r--r--browser/components/newtab/test/browser/browser_newtab_towindow.js45
-rw-r--r--browser/components/newtab/test/browser/browser_newtab_trigger.js50
-rw-r--r--browser/components/newtab/test/browser/browser_open_tab_focus.js38
-rw-r--r--browser/components/newtab/test/browser/browser_remote_l10n.js56
-rw-r--r--browser/components/newtab/test/browser/browser_topsites_annotation.js980
-rw-r--r--browser/components/newtab/test/browser/browser_topsites_contextMenu_options.js126
-rw-r--r--browser/components/newtab/test/browser/browser_topsites_section.js304
-rw-r--r--browser/components/newtab/test/browser/browser_trigger_messagesLoaded.js153
-rw-r--r--browser/components/newtab/test/browser/file_pdf.PDF12
-rw-r--r--browser/components/newtab/test/browser/head.js244
-rw-r--r--browser/components/newtab/test/browser/red_page.html6
-rw-r--r--browser/components/newtab/test/browser/redirect_to.sjs9
-rw-r--r--browser/components/newtab/test/browser/topstories.json11
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."
+ }
+ ]
+}