forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			251 lines
		
	
	
	
		
			8.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			251 lines
		
	
	
	
		
			8.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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.importESModule(
 | 
						|
  "resource:///modules/AboutNewTab.sys.mjs"
 | 
						|
);
 | 
						|
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");
 | 
						|
});
 |