summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheWorker.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheWorker.js')
-rw-r--r--browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheWorker.js251
1 files changed, 251 insertions, 0 deletions
diff --git a/browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheWorker.js b/browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheWorker.js
new file mode 100644
index 0000000000..0cbb81351b
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheWorker.js
@@ -0,0 +1,251 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test ensures that the about:home startup cache worker
+ * script can correctly convert a state object from the Activity
+ * Stream Redux store into an HTML document and script.
+ */
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+SearchTestUtils.init(this);
+AddonTestUtils.init(this);
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+const { AboutNewTab } = ChromeUtils.import(
+ "resource:///modules/AboutNewTab.jsm"
+);
+const { PREFS_CONFIG } = ChromeUtils.import(
+ "resource://activity-stream/lib/ActivityStream.jsm"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ BasePromiseWorker: "resource://gre/modules/PromiseWorker.sys.mjs",
+});
+
+const CACHE_WORKER_URL = "resource://activity-stream/lib/cache-worker.js";
+const NEWTAB_RENDER_URL =
+ "resource://activity-stream/data/content/newtab-render.js";
+
+/**
+ * In order to make this test less brittle, much of Activity Stream is
+ * initialized here in order to generate a state object at runtime, rather
+ * than hard-coding one in. This requires quite a bit of machinery in order
+ * to work properly. Specifically, we need to launch an HTTP server to serve
+ * a dynamic layout, and then have that layout point to a local feed rather
+ * than one from the Pocket CDN.
+ */
+add_setup(async function () {
+ do_get_profile();
+ // The SearchService is also needed in order to construct the initial state,
+ // which means that the AddonManager needs to be available.
+ await AddonTestUtils.promiseStartupManager();
+
+ // The example.com domain will be used to host the dynamic layout JSON and
+ // the top stories JSON.
+ let server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] });
+ server.registerDirectory("/", do_get_cwd());
+
+ // Top Stories are disabled by default in our testing profiles.
+ Services.prefs.setBoolPref(
+ "browser.newtabpage.activity-stream.feeds.section.topstories",
+ true
+ );
+ Services.prefs.setBoolPref(
+ "browser.newtabpage.activity-stream.feeds.system.topstories",
+ true
+ );
+
+ let defaultDSConfig = JSON.parse(
+ PREFS_CONFIG.get("discoverystream.config").getValue({
+ geo: "US",
+ locale: "en-US",
+ })
+ );
+
+ let newConfig = Object.assign(defaultDSConfig, {
+ show_spocs: false,
+ hardcoded_layout: false,
+ layout_endpoint: "http://example.com/ds_layout.json",
+ });
+
+ // 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(newConfig)
+ );
+
+ // We need to allow example.com as a place to get both the layout and the
+ // top stories from.
+ Services.prefs.setCharPref(
+ "browser.newtabpage.activity-stream.discoverystream.endpoints",
+ `http://example.com`
+ );
+
+ Services.prefs.setBoolPref(
+ "browser.newtabpage.activity-stream.telemetry.structuredIngestion",
+ false
+ );
+ Services.prefs.setBoolPref("browser.ping-centre.telemetry", false);
+
+ // We need a default search engine set up for rendering the search input.
+ await SearchTestUtils.installSearchExtension(
+ {
+ name: "Test engine",
+ keyword: "@testengine",
+ search_url_get_params: "s={searchTerms}",
+ },
+ { setAsDefault: true }
+ );
+
+ // Initialize Activity Stream, and pretend that a new window has been loaded
+ // to kick off initializing all of the feeds.
+ AboutNewTab.init();
+ 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;
+ });
+});
+
+/**
+ * Gets the Activity Stream Redux state from Activity Stream and sends it
+ * into an instance of the cache worker to ensure that the resulting markup
+ * and script makes sense.
+ */
+add_task(async function test_cache_worker() {
+ Services.prefs.setBoolPref(
+ "security.allow_parent_unrestricted_js_loads",
+ true
+ );
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads");
+ });
+
+ let state = AboutNewTab.activityStream.store.getState();
+
+ let cacheWorker = new BasePromiseWorker(CACHE_WORKER_URL);
+ let { page, script } = await cacheWorker.post("construct", [state]);
+ ok(!!page.length, "Got page content");
+ ok(!!script.length, "Got script content");
+
+ // The template strings should have been replaced.
+ equal(
+ page.indexOf("{{ MARKUP }}"),
+ -1,
+ "Page template should have {{ MARKUP }} replaced"
+ );
+ equal(
+ page.indexOf("{{ CACHE_TIME }}"),
+ -1,
+ "Page template should have {{ CACHE_TIME }} replaced"
+ );
+ equal(
+ script.indexOf("{{ STATE }}"),
+ -1,
+ "Script template should have {{ STATE }} replaced"
+ );
+
+ // Now let's make sure that the generated script makes sense. We'll
+ // evaluate it in a sandbox to make sure broken JS doesn't break the
+ // test.
+ let sandbox = Cu.Sandbox(Cu.getGlobalForObject({}));
+ let passedState = null;
+
+ // window.NewtabRenderUtils.renderCache is the exposed API from
+ // activity-stream.jsx that the script is expected to call to hydrate
+ // the pre-rendered markup. We'll implement that, and use that to ensure
+ // that the passed in state object matches the state we sent into the
+ // worker.
+ sandbox.window = {
+ NewtabRenderUtils: {
+ renderCache(aState) {
+ passedState = aState;
+ },
+ },
+ };
+ Cu.evalInSandbox(script, sandbox);
+
+ // The NEWTAB_RENDER_URL script is what ultimately causes the state
+ // to be passed into the renderCache function.
+ Services.scriptloader.loadSubScript(NEWTAB_RENDER_URL, sandbox);
+
+ equal(
+ sandbox.window.__FROM_STARTUP_CACHE__,
+ true,
+ "Should have set __FROM_STARTUP_CACHE__ to true"
+ );
+
+ // The worker is expected to modify the state slightly before running
+ // it through ReactDOMServer by setting App.isForStartupCache to true.
+ // This allows React components to change their behaviour if the cache
+ // is being generated.
+ state.App.isForStartupCache = true;
+
+ // Some of the properties on the state might have values set to undefined.
+ // There is no way to express a named undefined property on an object in
+ // JSON, so we filter those out by stringifying and re-parsing.
+ state = JSON.parse(JSON.stringify(state));
+
+ Assert.deepEqual(
+ passedState,
+ state,
+ "Should have called renderCache with the expected state"
+ );
+
+ // Now let's do a quick smoke-test on the markup to ensure that the
+ // one Top Story from topstories.json is there.
+ let parser = new DOMParser();
+ let doc = parser.parseFromString(page, "text/html");
+ let root = doc.getElementById("root");
+ ok(root.childElementCount, "There are children on the root node");
+
+ // There should be the 1 top story, and 2 placeholders.
+ equal(
+ Array.from(root.querySelectorAll(".ds-card")).length,
+ 3,
+ "There are 3 DSCards"
+ );
+ let cardHostname = doc.querySelector(
+ "[data-section-id='topstories'] .source"
+ ).innerText;
+ equal(cardHostname, "bbc.com", "Card hostname is bbc.com");
+
+ let placeholders = doc.querySelectorAll(".ds-card.placeholder");
+ equal(placeholders.length, 2, "There should be 2 placeholders");
+});
+
+/**
+ * Tests that if the cache-worker construct method throws an exception
+ * that the construct Promise still resolves. Passing a null state should
+ * be enough to get it to throw.
+ */
+add_task(async function test_cache_worker_exception() {
+ let cacheWorker = new BasePromiseWorker(CACHE_WORKER_URL);
+ let { page, script } = await cacheWorker.post("construct", [null]);
+ equal(page, null, "Should have gotten a null page nsIInputStream");
+ equal(script, null, "Should have gotten a null script nsIInputStream");
+});