tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

test_AboutHomeStartupCacheWorker.js (8443B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 /**
      7 * This test ensures that the about:home startup cache worker
      8 * script can correctly convert a state object from the Activity
      9 * Stream Redux store into an HTML document and script.
     10 */
     11 
     12 const { SearchTestUtils } = ChromeUtils.importESModule(
     13  "resource://testing-common/SearchTestUtils.sys.mjs"
     14 );
     15 const { TestUtils } = ChromeUtils.importESModule(
     16  "resource://testing-common/TestUtils.sys.mjs"
     17 );
     18 const { sinon } = ChromeUtils.importESModule(
     19  "resource://testing-common/Sinon.sys.mjs"
     20 );
     21 
     22 SearchTestUtils.init(this);
     23 
     24 const { AboutNewTab } = ChromeUtils.importESModule(
     25  "resource:///modules/AboutNewTab.sys.mjs"
     26 );
     27 
     28 ChromeUtils.defineESModuleGetters(this, {
     29  BasePromiseWorker: "resource://gre/modules/PromiseWorker.sys.mjs",
     30  DiscoveryStreamFeed: "resource://newtab/lib/DiscoveryStreamFeed.sys.mjs",
     31  PREFS_CONFIG: "resource://newtab/lib/ActivityStream.sys.mjs",
     32 });
     33 
     34 const CACHE_WORKER_URL = "resource://newtab/lib/cache.worker.js";
     35 const NEWTAB_RENDER_URL = "resource://newtab/data/content/newtab-render.js";
     36 
     37 /**
     38 * In order to make this test less brittle, much of Activity Stream is
     39 * initialized here in order to generate a state object at runtime, rather
     40 * than hard-coding one in. This requires quite a bit of machinery in order
     41 * to work properly. Specifically, we need to launch an HTTP server to serve
     42 * a dynamic layout, and then have that layout point to a local feed rather
     43 * than one from the Pocket CDN.
     44 */
     45 add_setup(async function () {
     46  do_get_profile();
     47  // The SearchService is also needed in order to construct the initial state,
     48  // which means that the AddonManager needs to be available.
     49  await SearchTestUtils.initXPCShellAddonManager();
     50 
     51  // The example.com domain will be used to host the dynamic layout JSON and
     52  // the top stories JSON.
     53  let server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] });
     54  server.registerDirectory("/", do_get_cwd());
     55 
     56  // Top Stories are disabled by default in our testing profiles.
     57  Services.prefs.setBoolPref(
     58    "browser.newtabpage.activity-stream.feeds.section.topstories",
     59    true
     60  );
     61  Services.prefs.setBoolPref(
     62    "browser.newtabpage.activity-stream.feeds.system.topstories",
     63    true
     64  );
     65  Services.prefs.setStringPref(
     66    "browser.newtabpage.activity-stream.discoverystream.region-weather-config",
     67    ""
     68  );
     69  Services.prefs.setBoolPref(
     70    "browser.newtabpage.activity-stream.newtabWallpapers.enabled",
     71    false
     72  );
     73 
     74  let defaultDSConfig = JSON.parse(
     75    PREFS_CONFIG.get("discoverystream.config").getValue({
     76      geo: "US",
     77      locale: "en-US",
     78    })
     79  );
     80  const sandbox = sinon.createSandbox();
     81  sandbox
     82    .stub(DiscoveryStreamFeed.prototype, "generateFeedUrl")
     83    .returns("http://example.com/topstories.json");
     84 
     85  // Configure Activity Stream to query for the layout JSON file that points
     86  // at the local top stories feed.
     87  Services.prefs.setCharPref(
     88    "browser.newtabpage.activity-stream.discoverystream.config",
     89    JSON.stringify(defaultDSConfig)
     90  );
     91 
     92  // We need to allow example.com as a place to get both the layout and the
     93  // top stories from.
     94  Services.prefs.setCharPref(
     95    "browser.newtabpage.activity-stream.discoverystream.endpoints",
     96    `http://example.com`
     97  );
     98 
     99  Services.prefs.setBoolPref(
    100    "browser.newtabpage.activity-stream.telemetry.structuredIngestion",
    101    false
    102  );
    103 
    104  // We need a default search engine set up for rendering the search input.
    105  await SearchTestUtils.installSearchExtension(
    106    {
    107      name: "Test engine",
    108      keyword: "@testengine",
    109      search_url_get_params: "s={searchTerms}",
    110    },
    111    { setAsDefault: true }
    112  );
    113 
    114  // Pretend that a new window has been loaded to kick off initializing all of
    115  // the feeds.
    116  AboutNewTab.onBrowserReady();
    117 
    118  // Much of Activity Stream initializes asynchronously. This is the easiest way
    119  // I could find to ensure that enough of the feeds had initialized to produce
    120  // a meaningful cached document.
    121  await TestUtils.waitForCondition(() => {
    122    let feed = AboutNewTab.activityStream.store.feeds.get(
    123      "feeds.discoverystreamfeed"
    124    );
    125    return feed?.loaded;
    126  });
    127 });
    128 
    129 /**
    130 * Gets the Activity Stream Redux state from Activity Stream and sends it
    131 * into an instance of the cache worker to ensure that the resulting markup
    132 * and script makes sense.
    133 */
    134 add_task(async function test_cache_worker() {
    135  Services.prefs.setBoolPref(
    136    "security.allow_parent_unrestricted_js_loads",
    137    true
    138  );
    139  registerCleanupFunction(() => {
    140    Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads");
    141  });
    142 
    143  let state = AboutNewTab.activityStream.store.getState();
    144 
    145  let cacheWorker = new BasePromiseWorker(CACHE_WORKER_URL);
    146  let { page, script } = await cacheWorker.post("construct", [state]);
    147  ok(!!page.length, "Got page content");
    148  ok(!!script.length, "Got script content");
    149 
    150  // The template strings should have been replaced.
    151  equal(
    152    page.indexOf("{{ MARKUP }}"),
    153    -1,
    154    "Page template should have {{ MARKUP }} replaced"
    155  );
    156  equal(
    157    page.indexOf("{{ CACHE_TIME }}"),
    158    -1,
    159    "Page template should have {{ CACHE_TIME }} replaced"
    160  );
    161  equal(
    162    script.indexOf("{{ STATE }}"),
    163    -1,
    164    "Script template should have {{ STATE }} replaced"
    165  );
    166 
    167  // Now let's make sure that the generated script makes sense. We'll
    168  // evaluate it in a sandbox to make sure broken JS doesn't break the
    169  // test.
    170  let sandbox = Cu.Sandbox(Cu.getGlobalForObject({}));
    171  let passedState = null;
    172 
    173  // window.NewtabRenderUtils.renderCache is the exposed API from
    174  // activity-stream.jsx that the script is expected to call to hydrate
    175  // the pre-rendered markup. We'll implement that, and use that to ensure
    176  // that the passed in state object matches the state we sent into the
    177  // worker.
    178  sandbox.window = {
    179    NewtabRenderUtils: {
    180      renderCache(aState) {
    181        passedState = aState;
    182      },
    183    },
    184  };
    185  Cu.evalInSandbox(script, sandbox);
    186 
    187  // The NEWTAB_RENDER_URL script is what ultimately causes the state
    188  // to be passed into the renderCache function.
    189  Services.scriptloader.loadSubScript(NEWTAB_RENDER_URL, sandbox);
    190 
    191  equal(
    192    sandbox.window.__FROM_STARTUP_CACHE__,
    193    true,
    194    "Should have set __FROM_STARTUP_CACHE__ to true"
    195  );
    196 
    197  // The worker is expected to modify the state slightly before running
    198  // it through ReactDOMServer by setting App.isForStartupCache to true.
    199  // This allows React components to change their behaviour if the cache
    200  // is being generated.
    201  state.App.isForStartupCache = {
    202    App: true,
    203    TopSites: true,
    204    DiscoveryStream: true,
    205    Weather: true,
    206    Wallpaper: true,
    207  };
    208 
    209  // Some of the properties on the state might have values set to undefined.
    210  // There is no way to express a named undefined property on an object in
    211  // JSON, so we filter those out by stringifying and re-parsing.
    212  state = JSON.parse(JSON.stringify(state));
    213 
    214  Assert.deepEqual(
    215    passedState,
    216    state,
    217    "Should have called renderCache with the expected state"
    218  );
    219 
    220  // Now let's do a quick smoke-test on the markup to ensure that the
    221  // one Top Story from topstories.json is there.
    222  let parser = new DOMParser();
    223  let doc = parser.parseFromString(page, "text/html");
    224  let root = doc.getElementById("root");
    225  ok(root.childElementCount, "There are children on the root node");
    226 
    227  // There should be the 1 top story, and 23 placeholders.
    228  equal(
    229    Array.from(root.querySelectorAll(".ds-card")).length,
    230    24,
    231    "There are 24 DSCards"
    232  );
    233  let cardHostname = doc.querySelector(
    234    "[data-section-id='topstories'] .source"
    235  ).innerText;
    236  equal(cardHostname, "bbc.com", "Card hostname is bbc.com");
    237 
    238  let placeholders = doc.querySelectorAll(".ds-card.placeholder");
    239  equal(placeholders.length, 23, "There should be 23 placeholders");
    240 });
    241 
    242 /**
    243 * Tests that if the cache-worker construct method throws an exception
    244 * that the construct Promise still resolves. Passing a null state should
    245 * be enough to get it to throw.
    246 */
    247 add_task(async function test_cache_worker_exception() {
    248  let cacheWorker = new BasePromiseWorker(CACHE_WORKER_URL);
    249  let { page, script } = await cacheWorker.post("construct", [null]);
    250  equal(page, null, "Should have gotten a null page nsIInputStream");
    251  equal(script, null, "Should have gotten a null script nsIInputStream");
    252 });