tor-browser

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

test_top_sites.js (74793B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 const { TopSites, insertPinned, DEFAULT_TOP_SITES } =
      7  ChromeUtils.importESModule("resource:///modules/topsites/TopSites.sys.mjs");
      8 
      9 ChromeUtils.defineESModuleGetters(this, {
     10  FilterAdult: "resource:///modules/FilterAdult.sys.mjs",
     11  NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
     12  NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
     13  sinon: "resource://testing-common/Sinon.sys.mjs",
     14  PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
     15  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     16  SearchService: "resource://gre/modules/SearchService.sys.mjs",
     17  TestUtils: "resource://testing-common/TestUtils.sys.mjs",
     18  TOP_SITES_DEFAULT_ROWS: "resource:///modules/topsites/constants.mjs",
     19  TOP_SITES_MAX_SITES_PER_ROW: "resource:///modules/topsites/constants.mjs",
     20 });
     21 
     22 const FAKE_FAVICON = "data987";
     23 const FAKE_FAVICON_SIZE = 128;
     24 // Two visits on the same day.
     25 const FAKE_FRECENCY = PlacesUtils.history.pageFrecencyThreshold(0, 2, false);
     26 const FAKE_LINKS = new Array(2 * TOP_SITES_MAX_SITES_PER_ROW)
     27  .fill(null)
     28  .map((v, i) => ({
     29    frecency: FAKE_FRECENCY,
     30    url: `http://www.site${i}.com`,
     31  }));
     32 
     33 function FakeTippyTopProvider() {}
     34 FakeTippyTopProvider.prototype = {
     35  async init() {
     36    this.initialized = true;
     37  },
     38  processSite(site) {
     39    return site;
     40  },
     41 };
     42 
     43 let gSearchServiceInitStub;
     44 let gGetTopSitesStub;
     45 
     46 function stubTopSites(sandbox) {
     47  async function cleanup() {
     48    if (TopSites._refreshing) {
     49      info("Wait for refresh to finish.");
     50      // Wait for refresh to finish or else removing the store while a process
     51      // is running will result in errors.
     52      await TestUtils.topicObserved("topsites-refreshed");
     53      info("Top sites was refreshed.");
     54    }
     55    TopSites._tippyTopProvider.initialized = false;
     56    TopSites.pinnedCache.clear();
     57    TopSites.frecentCache.clear();
     58    TopSites._reset();
     59    stub.restore();
     60    info("Finished cleaning up TopSites.");
     61  }
     62 
     63  // To avoid having to setup search for each test, we stub this method and
     64  // unstub it when the unit test calls for the search shortcuts.
     65  let stub = sandbox.stub(TopSites, "updateCustomSearchShortcuts");
     66 
     67  TopSites._requestRichIcon = sandbox.stub();
     68  // Set preferences to match the store state.
     69  Services.prefs.setIntPref(
     70    "browser.newtabpage.activity-stream.topSitesRows",
     71    2
     72  );
     73  info("Created mock store for TopSites.");
     74  return cleanup;
     75 }
     76 
     77 function createExpectedPinnedLink(link, index) {
     78  link.isDefault = false;
     79  link.isPinned = true;
     80  link.searchTopSite = false;
     81  link.favicon = FAKE_FAVICON;
     82  link.faviconSize = FAKE_FAVICON_SIZE;
     83  link.pinIndex = index;
     84  return link;
     85 }
     86 
     87 function assertLinks(actualLinks, expectedLinks) {
     88  Assert.equal(
     89    actualLinks.length,
     90    expectedLinks.length,
     91    "Links have equal length."
     92  );
     93  for (let i = 0; i < actualLinks.length; ++i) {
     94    Assert.deepEqual(actualLinks[i], expectedLinks[i], "Link entry matches");
     95  }
     96 }
     97 
     98 add_setup(async () => {
     99  // Places requires a profile.
    100  do_get_profile();
    101 
    102  let sandbox = sinon.createSandbox();
    103  sandbox.stub(SearchService.prototype, "defaultEngine").get(() => {
    104    return { identifier: "ddg", searchUrlDomain: "duckduckgo.com" };
    105  });
    106 
    107  gGetTopSitesStub = sandbox
    108    .stub(NewTabUtils.activityStreamLinks, "getTopSites")
    109    .resolves(FAKE_LINKS);
    110 
    111  gSearchServiceInitStub = sandbox
    112    .stub(SearchService.prototype, "init")
    113    .resolves();
    114 
    115  sandbox.stub(NewTabUtils.activityStreamProvider, "_faviconBytesToDataURI");
    116 
    117  sandbox
    118    .stub(NewTabUtils.activityStreamProvider, "_addFavicons")
    119    .callsFake(l => {
    120      return Promise.resolve(
    121        l.map(link => {
    122          link.favicon = FAKE_FAVICON;
    123          link.faviconSize = FAKE_FAVICON_SIZE;
    124          return link;
    125        })
    126      );
    127    });
    128 
    129  registerCleanupFunction(() => {
    130    sandbox.restore();
    131  });
    132 });
    133 
    134 add_task(async function test_construction() {
    135  Assert.ok(TopSites._currentSearchHostname, "_currentSearchHostname defined");
    136 });
    137 
    138 add_task(async function test_refreshDefaults() {
    139  let sandbox = sinon.createSandbox();
    140  let cleanup = stubTopSites(sandbox);
    141  Assert.ok(
    142    !DEFAULT_TOP_SITES.length,
    143    "Should have 0 DEFAULT_TOP_SITES initially."
    144  );
    145 
    146  // We have to init to subscribe to changes to the preferences.
    147  await TopSites.init();
    148 
    149  info(
    150    "TopSites.refreshDefaults should add defaults on default.sites pref change."
    151  );
    152  Services.prefs.setStringPref(
    153    "browser.newtabpage.activity-stream.default.sites",
    154    "https://foo.com"
    155  );
    156 
    157  Assert.equal(
    158    DEFAULT_TOP_SITES.length,
    159    1,
    160    "Should have 1 DEFAULT_TOP_SITES now."
    161  );
    162 
    163  // Reset the DEFAULT_TOP_SITES;
    164  DEFAULT_TOP_SITES.length = 0;
    165 
    166  info("refreshDefaults should refresh on topSiteRows PREF_CHANGED");
    167  let refreshStub = sandbox.stub(TopSites, "refresh");
    168  Services.prefs.setIntPref(
    169    "browser.newtabpage.activity-stream.topSitesRows",
    170    1
    171  );
    172  Assert.ok(TopSites.refresh.calledOnce, "refresh called");
    173  refreshStub.restore();
    174 
    175  // Reset the DEFAULT_TOP_SITES;
    176  DEFAULT_TOP_SITES.length = 0;
    177 
    178  info("refreshDefaults should have default sites with .isDefault = true");
    179  TopSites.refreshDefaults("https://foo.com");
    180  Assert.equal(
    181    DEFAULT_TOP_SITES.length,
    182    1,
    183    "Should have a DEFAULT_TOP_SITES now."
    184  );
    185  Assert.ok(
    186    DEFAULT_TOP_SITES[0].isDefault,
    187    "Lone top site should be the default."
    188  );
    189 
    190  // Reset the DEFAULT_TOP_SITES;
    191  DEFAULT_TOP_SITES.length = 0;
    192 
    193  info("refreshDefaults should have default sites with appropriate hostname");
    194  TopSites.refreshDefaults("https://foo.com");
    195  Assert.equal(
    196    DEFAULT_TOP_SITES.length,
    197    1,
    198    "Should have a DEFAULT_TOP_SITES now."
    199  );
    200  let [site] = DEFAULT_TOP_SITES;
    201  Assert.equal(
    202    site.hostname,
    203    NewTabUtils.shortURL(site),
    204    "Lone top site should have the right hostname."
    205  );
    206 
    207  // Reset the DEFAULT_TOP_SITES;
    208  DEFAULT_TOP_SITES.length = 0;
    209 
    210  info("refreshDefaults should add no defaults on empty pref");
    211  TopSites.refreshDefaults("");
    212  Assert.equal(
    213    DEFAULT_TOP_SITES.length,
    214    0,
    215    "Should have 0 DEFAULT_TOP_SITES now."
    216  );
    217 
    218  info("refreshDefaults should be able to clear defaults");
    219  TopSites.refreshDefaults("https://foo.com");
    220  TopSites.refreshDefaults("");
    221 
    222  Assert.equal(
    223    DEFAULT_TOP_SITES.length,
    224    0,
    225    "Should have 0 DEFAULT_TOP_SITES now."
    226  );
    227 
    228  Services.prefs.clearUserPref(
    229    "browser.newtabpage.activity-stream.default.sites"
    230  );
    231  Services.prefs.clearUserPref(
    232    "browser.newtabpage.activity-stream.topSitesRows"
    233  );
    234  TopSites.uninit();
    235  sandbox.restore();
    236  await cleanup();
    237 });
    238 
    239 add_task(
    240  async function test_getLinksWithDefaults_on_SearchService_init_failure() {
    241    let sandbox = sinon.createSandbox();
    242    let cleanup = stubTopSites(sandbox);
    243 
    244    TopSites.refreshDefaults("https://foo.com");
    245 
    246    gSearchServiceInitStub.rejects(
    247      new Error("Simulating search init failures")
    248    );
    249 
    250    const result = await TopSites.getLinksWithDefaults();
    251    Assert.ok(result);
    252 
    253    gSearchServiceInitStub.resolves();
    254 
    255    sandbox.restore();
    256    await cleanup();
    257  }
    258 );
    259 
    260 add_task(async function test_getLinksWithDefaults() {
    261  NewTabUtils.activityStreamLinks.getTopSites.resetHistory();
    262 
    263  let sandbox = sinon.createSandbox();
    264  let cleanup = stubTopSites(sandbox);
    265 
    266  TopSites.refreshDefaults("https://foo.com");
    267 
    268  info("getLinksWithDefaults should get the links from NewTabUtils");
    269  let result = await TopSites.getLinksWithDefaults();
    270 
    271  const reference = FAKE_LINKS.map(site =>
    272    Object.assign({}, site, {
    273      hostname: NewTabUtils.shortURL(site),
    274      typedBonus: true,
    275    })
    276  );
    277 
    278  Assert.deepEqual(result, reference);
    279  Assert.ok(NewTabUtils.activityStreamLinks.getTopSites.calledOnce);
    280 
    281  info("getLinksWithDefaults should indicate the links get typed bonus");
    282  Assert.ok(result[0].typedBonus, "Expected typed bonus property to be true.");
    283 
    284  sandbox.restore();
    285  await cleanup();
    286 });
    287 
    288 add_task(async function test_getLinksWithDefaults_filterAdult() {
    289  let sandbox = sinon.createSandbox();
    290  info("getLinksWithDefaults should filter out non-pinned adult sites");
    291 
    292  sandbox.stub(FilterAdult, "filter").returns([]);
    293  const TEST_URL = "https://foo.com/";
    294  sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [{ url: TEST_URL }]);
    295 
    296  let cleanup = stubTopSites(sandbox);
    297  TopSites.refreshDefaults("https://foo.com");
    298 
    299  const result = await TopSites.getLinksWithDefaults();
    300  Assert.ok(FilterAdult.filter.calledOnce);
    301  Assert.equal(result.length, 1);
    302  Assert.equal(result[0].url, TEST_URL);
    303 
    304  sandbox.restore();
    305  await cleanup();
    306 });
    307 
    308 add_task(async function test_getLinksWithDefaults_caching() {
    309  let sandbox = sinon.createSandbox();
    310 
    311  info(
    312    "getLinksWithDefaults should filter out the defaults that have been blocked"
    313  );
    314  // make sure we only have one top site, and we block the only default site we have to show
    315  const url = "www.myonlytopsite.com";
    316  const topsite = {
    317    frecency: FAKE_FRECENCY,
    318    hostname: NewTabUtils.shortURL({ url }),
    319    typedBonus: true,
    320    url,
    321  };
    322 
    323  const blockedDefaultSite = { url: "https://foo.com" };
    324  gGetTopSitesStub.resolves([topsite]);
    325  sandbox.stub(NewTabUtils.blockedLinks, "isBlocked").callsFake(site => {
    326    return site.url === blockedDefaultSite.url;
    327  });
    328 
    329  let cleanup = stubTopSites(sandbox);
    330  TopSites.refreshDefaults("https://foo.com");
    331  const result = await TopSites.getLinksWithDefaults();
    332 
    333  // what we should be left with is just the top site we added, and not the default site we blocked
    334  Assert.equal(result.length, 1);
    335  Assert.deepEqual(result[0], topsite);
    336  let foundBlocked = result.find(site => site.url === blockedDefaultSite.url);
    337  Assert.ok(!foundBlocked, "Should not have found blocked site.");
    338 
    339  gGetTopSitesStub.resolves(FAKE_LINKS);
    340  sandbox.restore();
    341  await cleanup();
    342 });
    343 
    344 add_task(async function test_getLinksWithDefaults_dedupe() {
    345  let sandbox = sinon.createSandbox();
    346 
    347  info("getLinksWithDefaults should call dedupe.group on the links");
    348  let cleanup = stubTopSites(sandbox);
    349  TopSites.refreshDefaults("https://foo.com");
    350 
    351  let stub = sandbox.stub(TopSites.dedupe, "group").callsFake((...id) => id);
    352  await TopSites.getLinksWithDefaults();
    353 
    354  Assert.ok(stub.calledOnce, "dedupe.group was called once");
    355  sandbox.restore();
    356  await cleanup();
    357 });
    358 
    359 add_task(async function test__dedupe_key() {
    360  let sandbox = sinon.createSandbox();
    361 
    362  info("_dedupeKey should dedupe on hostname instead of url");
    363  let cleanup = stubTopSites(sandbox);
    364  TopSites.refreshDefaults("https://foo.com");
    365 
    366  let site = { url: "foo", hostname: "bar" };
    367  let result = TopSites._dedupeKey(site);
    368 
    369  Assert.equal(result, site.hostname, "deduped on hostname");
    370  sandbox.restore();
    371  await cleanup();
    372 });
    373 
    374 add_task(async function test_getLinksWithDefaults_adds_defaults() {
    375  let sandbox = sinon.createSandbox();
    376 
    377  info(
    378    "getLinksWithDefaults should add defaults if there are are not enough links"
    379  );
    380  const TEST_LINKS = [{ frecency: FAKE_FRECENCY, url: "foo.com" }];
    381  gGetTopSitesStub.resolves(TEST_LINKS);
    382  let cleanup = stubTopSites(sandbox);
    383  TopSites.refreshDefaults("https://foo.com");
    384 
    385  let result = await TopSites.getLinksWithDefaults();
    386 
    387  let reference = [...TEST_LINKS, ...DEFAULT_TOP_SITES].map(s =>
    388    Object.assign({}, s, {
    389      hostname: NewTabUtils.shortURL(s),
    390      typedBonus: true,
    391    })
    392  );
    393 
    394  Assert.deepEqual(result, reference);
    395 
    396  gGetTopSitesStub.resolves(FAKE_LINKS);
    397  sandbox.restore();
    398  await cleanup();
    399 });
    400 
    401 add_task(
    402  async function test_getLinksWithDefaults_adds_defaults_for_visible_slots() {
    403    let sandbox = sinon.createSandbox();
    404 
    405    info(
    406      "getLinksWithDefaults should only add defaults up to the number of visible slots"
    407    );
    408    const numVisible = TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW;
    409    let testLinks = [];
    410    for (let i = 0; i < numVisible - 1; i++) {
    411      testLinks.push({ frecency: FAKE_FRECENCY, url: `foo${i}.com` });
    412    }
    413    gGetTopSitesStub.resolves(testLinks);
    414 
    415    let cleanup = stubTopSites(sandbox);
    416    TopSites.refreshDefaults("https://foo.com");
    417 
    418    let result = await TopSites.getLinksWithDefaults();
    419 
    420    let reference = [...testLinks, DEFAULT_TOP_SITES[0]].map(s =>
    421      Object.assign({}, s, {
    422        hostname: NewTabUtils.shortURL(s),
    423        typedBonus: true,
    424      })
    425    );
    426 
    427    Assert.equal(result.length, numVisible);
    428    Assert.deepEqual(result, reference);
    429 
    430    gGetTopSitesStub.resolves(FAKE_LINKS);
    431    sandbox.restore();
    432    await cleanup();
    433  }
    434 );
    435 
    436 add_task(async function test_getLinksWithDefaults_no_throw_on_no_links() {
    437  let sandbox = sinon.createSandbox();
    438 
    439  info("getLinksWithDefaults should not throw if NewTabUtils returns null");
    440  gGetTopSitesStub.resolves(null);
    441 
    442  let cleanup = stubTopSites(sandbox);
    443  TopSites.refreshDefaults("https://foo.com");
    444 
    445  await TopSites.getLinksWithDefaults();
    446  Assert.ok(true, "getLinksWithDefaults did not throw");
    447 
    448  gGetTopSitesStub.resolves(FAKE_LINKS);
    449  sandbox.restore();
    450  await cleanup();
    451 });
    452 
    453 add_task(async function test_getLinksWithDefaults_get_more_on_request() {
    454  let sandbox = sinon.createSandbox();
    455 
    456  info("getLinksWithDefaults should get more if the user has asked for more");
    457  let testLinks = new Array(4 * TOP_SITES_MAX_SITES_PER_ROW)
    458    .fill(null)
    459    .map((v, i) => ({
    460      frecency: FAKE_FRECENCY,
    461      url: `http://www.site${i}.com`,
    462    }));
    463  gGetTopSitesStub.resolves(testLinks);
    464 
    465  let cleanup = stubTopSites(sandbox);
    466  TopSites.refreshDefaults("https://foo.com");
    467 
    468  const TEST_ROWS = 3;
    469  Services.prefs.setIntPref(
    470    "browser.newtabpage.activity-stream.topSitesRows",
    471    TEST_ROWS
    472  );
    473 
    474  let result = await TopSites.getLinksWithDefaults();
    475  Assert.equal(result.length, TEST_ROWS * TOP_SITES_MAX_SITES_PER_ROW);
    476 
    477  Services.prefs.clearUserPref(
    478    "browser.newtabpage.activity-stream.topSitesRows"
    479  );
    480  gGetTopSitesStub.resolves(FAKE_LINKS);
    481  sandbox.restore();
    482  await cleanup();
    483 });
    484 
    485 add_task(async function test_getLinksWithDefaults_reuse_cache() {
    486  let sandbox = sinon.createSandbox();
    487  info("getLinksWithDefaults should reuse the cache on subsequent calls");
    488 
    489  let cleanup = stubTopSites(sandbox);
    490  TopSites.refreshDefaults("https://foo.com");
    491 
    492  gGetTopSitesStub.resetHistory();
    493 
    494  await TopSites.getLinksWithDefaults();
    495  await TopSites.getLinksWithDefaults();
    496 
    497  Assert.ok(
    498    NewTabUtils.activityStreamLinks.getTopSites.calledOnce,
    499    "getTopSites only called once"
    500  );
    501 
    502  sandbox.restore();
    503  await cleanup();
    504 });
    505 
    506 add_task(
    507  async function test_getLinksWithDefaults_ignore_cache_on_requesting_more() {
    508    let sandbox = sinon.createSandbox();
    509    info("getLinksWithDefaults should ignore the cache when requesting more");
    510 
    511    let cleanup = stubTopSites(sandbox);
    512    TopSites.refreshDefaults("https://foo.com");
    513 
    514    gGetTopSitesStub.resetHistory();
    515 
    516    await TopSites.getLinksWithDefaults();
    517    Services.prefs.setIntPref(
    518      "browser.newtabpage.activity-stream.topSitesRows",
    519      3
    520    );
    521    await TopSites.getLinksWithDefaults();
    522 
    523    Assert.ok(
    524      NewTabUtils.activityStreamLinks.getTopSites.calledTwice,
    525      "getTopSites called twice"
    526    );
    527 
    528    Services.prefs.clearUserPref(
    529      "browser.newtabpage.activity-stream.topSitesRows"
    530    );
    531    sandbox.restore();
    532    await cleanup();
    533  }
    534 );
    535 
    536 add_task(
    537  async function test_getLinksWithDefaults_migrate_pinned_favicon_data() {
    538    let sandbox = sinon.createSandbox();
    539    info(
    540      "getLinksWithDefaults should migrate pinned favicon data without getting favicons again"
    541    );
    542 
    543    let cleanup = stubTopSites(sandbox);
    544    TopSites.refreshDefaults("https://foo.com");
    545 
    546    gGetTopSitesStub.resetHistory();
    547 
    548    sandbox
    549      .stub(NewTabUtils.pinnedLinks, "links")
    550      .get(() => [{ url: "https://foo.com/" }]);
    551 
    552    await TopSites.getLinksWithDefaults();
    553 
    554    let originalCallCount =
    555      NewTabUtils.activityStreamProvider._addFavicons.callCount;
    556    TopSites.pinnedCache.expire();
    557 
    558    let result = await TopSites.getLinksWithDefaults();
    559 
    560    Assert.equal(
    561      NewTabUtils.activityStreamProvider._addFavicons.callCount,
    562      originalCallCount,
    563      "_addFavicons was not called again."
    564    );
    565    Assert.equal(result[0].favicon, FAKE_FAVICON);
    566    Assert.equal(result[0].faviconSize, FAKE_FAVICON_SIZE);
    567 
    568    sandbox.restore();
    569    await cleanup();
    570  }
    571 );
    572 
    573 add_task(async function test_getLinksWithDefaults_no_internal_properties() {
    574  let sandbox = sinon.createSandbox();
    575  info("getLinksWithDefaults should not expose internal link properties");
    576 
    577  let cleanup = stubTopSites(sandbox);
    578  TopSites.refreshDefaults("https://foo.com");
    579 
    580  let result = await TopSites.getLinksWithDefaults();
    581 
    582  let internal = Object.keys(result[0]).filter(key => key.startsWith("__"));
    583  Assert.equal(internal.join(""), "");
    584 
    585  sandbox.restore();
    586  await cleanup();
    587 });
    588 
    589 add_task(async function test_getLinksWithDefaults_copy_frecent_screenshot() {
    590  // TopSites pulls data from NewTabUtils.activityStreamLinks.getTopSites()
    591  // which can still pass screenshots to it if they are available.
    592  let sandbox = sinon.createSandbox();
    593  info(
    594    "getLinksWithDefaults should copy the screenshot of the frecent site if it exists"
    595  );
    596 
    597  let cleanup = stubTopSites(sandbox);
    598  TopSites.refreshDefaults("https://foo.com");
    599 
    600  const TEST_SCREENSHOT = "screenshot";
    601 
    602  gGetTopSitesStub.resolves([
    603    { url: "https://foo.com/", screenshot: TEST_SCREENSHOT },
    604  ]);
    605  sandbox
    606    .stub(NewTabUtils.pinnedLinks, "links")
    607    .get(() => [{ url: "https://foo.com/" }]);
    608 
    609  let result = await TopSites.getLinksWithDefaults();
    610 
    611  Assert.equal(result[0].screenshot, TEST_SCREENSHOT);
    612 
    613  gGetTopSitesStub.resolves(FAKE_LINKS);
    614  sandbox.restore();
    615  await cleanup();
    616 });
    617 
    618 add_task(async function test_getLinksWithDefaults_copies_both_screenshots() {
    619  // TopSites pulls data from NewTabUtils.activityStreamLinks.getTopSites()
    620  // and NewTabUtils.pinnedLinks which can pass screenshot data to it if they
    621  // are available.
    622  let sandbox = sinon.createSandbox();
    623  info(
    624    "getLinksWithDefaults should still copy the frecent screenshot if " +
    625      "customScreenshotURL is set"
    626  );
    627 
    628  let cleanup = stubTopSites(sandbox);
    629  TopSites.refreshDefaults("https://foo.com");
    630 
    631  gGetTopSitesStub.resolves([
    632    { url: "https://foo.com/", screenshot: "screenshot" },
    633  ]);
    634  sandbox
    635    .stub(NewTabUtils.pinnedLinks, "links")
    636    .get(() => [{ url: "https://foo.com/", customScreenshotURL: "custom" }]);
    637 
    638  let result = await TopSites.getLinksWithDefaults();
    639 
    640  Assert.equal(result[0].screenshot, "screenshot");
    641  Assert.equal(result[0].customScreenshotURL, "custom");
    642 
    643  gGetTopSitesStub.resolves(FAKE_LINKS);
    644  sandbox.restore();
    645  await cleanup();
    646 });
    647 
    648 add_task(async function test_getLinksWithDefaults_persist_screenshot() {
    649  let sandbox = sinon.createSandbox();
    650  info(
    651    "getLinksWithDefaults should keep the same screenshot if no frecent site is found"
    652  );
    653 
    654  let cleanup = stubTopSites(sandbox);
    655  TopSites.refreshDefaults("https://foo.com");
    656 
    657  const CUSTOM_SCREENSHOT = "custom";
    658 
    659  gGetTopSitesStub.resolves([]);
    660  sandbox
    661    .stub(NewTabUtils.pinnedLinks, "links")
    662    .get(() => [{ url: "https://foo.com/", screenshot: CUSTOM_SCREENSHOT }]);
    663 
    664  let result = await TopSites.getLinksWithDefaults();
    665 
    666  Assert.equal(result[0].screenshot, CUSTOM_SCREENSHOT);
    667 
    668  gGetTopSitesStub.resolves(FAKE_LINKS);
    669  sandbox.restore();
    670  await cleanup();
    671 });
    672 
    673 add_task(
    674  async function test_getLinksWithDefaults_no_overwrite_pinned_screenshot() {
    675    let sandbox = sinon.createSandbox();
    676    info("getLinksWithDefaults should not overwrite pinned site screenshot");
    677 
    678    let cleanup = stubTopSites(sandbox);
    679    TopSites.refreshDefaults("https://foo.com");
    680 
    681    const EXISTING_SCREENSHOT = "some-screenshot";
    682 
    683    gGetTopSitesStub.resolves([{ url: "https://foo.com/", screenshot: "foo" }]);
    684    sandbox
    685      .stub(NewTabUtils.pinnedLinks, "links")
    686      .get(() => [
    687        { url: "https://foo.com/", screenshot: EXISTING_SCREENSHOT },
    688      ]);
    689 
    690    let result = await TopSites.getLinksWithDefaults();
    691 
    692    Assert.equal(result[0].screenshot, EXISTING_SCREENSHOT);
    693 
    694    gGetTopSitesStub.resolves(FAKE_LINKS);
    695    sandbox.restore();
    696    await cleanup();
    697  }
    698 );
    699 
    700 add_task(
    701  async function test_getLinksWithDefaults_no_searchTopSite_from_frecent() {
    702    let sandbox = sinon.createSandbox();
    703    info("getLinksWithDefaults should not set searchTopSite from frecent site");
    704 
    705    let cleanup = stubTopSites(sandbox);
    706    TopSites.refreshDefaults("https://foo.com");
    707 
    708    const EXISTING_SCREENSHOT = "some-screenshot";
    709 
    710    gGetTopSitesStub.resolves([
    711      {
    712        url: "https://foo.com/",
    713        searchTopSite: true,
    714        screenshot: EXISTING_SCREENSHOT,
    715      },
    716    ]);
    717    sandbox
    718      .stub(NewTabUtils.pinnedLinks, "links")
    719      .get(() => [{ url: "https://foo.com/" }]);
    720 
    721    let result = await TopSites.getLinksWithDefaults();
    722 
    723    Assert.ok(!result[0].searchTopSite);
    724    // But it should copy over other properties
    725    Assert.equal(result[0].screenshot, EXISTING_SCREENSHOT);
    726 
    727    gGetTopSitesStub.resolves(FAKE_LINKS);
    728    sandbox.restore();
    729    await cleanup();
    730  }
    731 );
    732 
    733 add_task(async function test_getLinksWithDefaults_concurrency_getTopSites() {
    734  let sandbox = sinon.createSandbox();
    735  info(
    736    "getLinksWithDefaults concurrent calls should call the backing data once"
    737  );
    738 
    739  let cleanup = stubTopSites(sandbox);
    740  TopSites.refreshDefaults("https://foo.com");
    741 
    742  NewTabUtils.activityStreamLinks.getTopSites.resetHistory();
    743 
    744  await Promise.all([
    745    TopSites.getLinksWithDefaults(),
    746    TopSites.getLinksWithDefaults(),
    747  ]);
    748 
    749  Assert.ok(
    750    NewTabUtils.activityStreamLinks.getTopSites.calledOnce,
    751    "getTopSites only called once"
    752  );
    753 
    754  sandbox.restore();
    755  await cleanup();
    756 });
    757 
    758 add_task(async function test_getLinksWithDefaults_deduping_no_dedupe_pinned() {
    759  let sandbox = sinon.createSandbox();
    760  info("getLinksWithDefaults should not dedupe pinned sites");
    761 
    762  let cleanup = stubTopSites(sandbox);
    763  TopSites.refreshDefaults("https://foo.com");
    764 
    765  sandbox
    766    .stub(NewTabUtils.pinnedLinks, "links")
    767    .get(() => [
    768      { url: "https://developer.mozilla.org/en-US/docs/Web" },
    769      { url: "https://developer.mozilla.org/en-US/docs/Learn" },
    770    ]);
    771 
    772  let sites = await TopSites.getLinksWithDefaults();
    773  Assert.equal(sites.length, 2 * TOP_SITES_MAX_SITES_PER_ROW);
    774  Assert.equal(sites[0].url, NewTabUtils.pinnedLinks.links[0].url);
    775  Assert.equal(sites[1].url, NewTabUtils.pinnedLinks.links[1].url);
    776  Assert.equal(sites[0].hostname, sites[1].hostname);
    777 
    778  sandbox.restore();
    779  await cleanup();
    780 });
    781 
    782 add_task(async function test_getLinksWithDefaults_prefer_pinned_sites() {
    783  let sandbox = sinon.createSandbox();
    784 
    785  info("getLinksWithDefaults should prefer pinned sites over links");
    786 
    787  let cleanup = stubTopSites(sandbox);
    788  TopSites.refreshDefaults();
    789 
    790  sandbox
    791    .stub(NewTabUtils.pinnedLinks, "links")
    792    .get(() => [
    793      { url: "https://developer.mozilla.org/en-US/docs/Web" },
    794      { url: "https://developer.mozilla.org/en-US/docs/Learn" },
    795    ]);
    796 
    797  const SECOND_TOP_SITE_URL = "https://www.mozilla.org/";
    798 
    799  gGetTopSitesStub.resolves([
    800    { frecency: FAKE_FRECENCY, url: "https://developer.mozilla.org/" },
    801    { frecency: FAKE_FRECENCY, url: SECOND_TOP_SITE_URL },
    802  ]);
    803 
    804  let sites = await TopSites.getLinksWithDefaults();
    805 
    806  // Expecting 3 links where there's 2 pinned and 1 www.mozilla.org, so
    807  // the frecent with matching hostname as pinned is removed.
    808  Assert.equal(sites.length, 3);
    809  Assert.equal(sites[0].url, NewTabUtils.pinnedLinks.links[0].url);
    810  Assert.equal(sites[1].url, NewTabUtils.pinnedLinks.links[1].url);
    811  Assert.equal(sites[2].url, SECOND_TOP_SITE_URL);
    812 
    813  gGetTopSitesStub.resolves(FAKE_LINKS);
    814  sandbox.restore();
    815  await cleanup();
    816 });
    817 
    818 add_task(async function test_getLinksWithDefaults_title_and_null() {
    819  let sandbox = sinon.createSandbox();
    820 
    821  info("getLinksWithDefaults should return sites that have a title");
    822 
    823  let cleanup = stubTopSites(sandbox);
    824  TopSites.refreshDefaults();
    825 
    826  sandbox
    827    .stub(NewTabUtils.pinnedLinks, "links")
    828    .get(() => [{ url: "https://github.com/mozilla/activity-stream" }]);
    829 
    830  let sites = await TopSites.getLinksWithDefaults();
    831  for (let site of sites) {
    832    Assert.ok(site.hostname);
    833  }
    834 
    835  info("getLinksWithDefaults should not throw for null entries");
    836  sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [null]);
    837  await TopSites.getLinksWithDefaults();
    838  Assert.ok(true, "getLinksWithDefaults didn't throw");
    839 
    840  sandbox.restore();
    841  await cleanup();
    842 });
    843 
    844 add_task(async function test_getLinksWithDefaults_calls__fetchIcon() {
    845  let sandbox = sinon.createSandbox();
    846 
    847  info("getLinksWithDefaults should return sites that have a title");
    848 
    849  let cleanup = stubTopSites(sandbox);
    850  TopSites.refreshDefaults();
    851 
    852  sandbox.spy(TopSites, "_fetchIcon");
    853  let results = await TopSites.getLinksWithDefaults();
    854  Assert.ok(results.length, "Got back some results");
    855  Assert.equal(TopSites._fetchIcon.callCount, results.length);
    856  for (let result of results) {
    857    Assert.ok(TopSites._fetchIcon.calledWith(result));
    858  }
    859 
    860  sandbox.restore();
    861  await cleanup();
    862 });
    863 
    864 add_task(async function test_init() {
    865  let sandbox = sinon.createSandbox();
    866 
    867  sandbox.stub(NimbusFeatures.newtab, "onUpdate");
    868 
    869  let cleanup = stubTopSites(sandbox);
    870 
    871  sandbox.stub(TopSites, "refresh");
    872  await TopSites.init();
    873 
    874  info("TopSites.init should call refresh");
    875  Assert.ok(TopSites.refresh.calledOnce, "refresh called once");
    876  Assert.ok(
    877    TopSites.refresh.calledWithExactly({
    878      isStartup: true,
    879    })
    880  );
    881 
    882  TopSites.uninit();
    883  sandbox.restore();
    884  await cleanup();
    885 });
    886 
    887 /**
    888 * If multiple callers are attempting to initializing TopSites, we should
    889 * initialize only once and wait until its completed.
    890 */
    891 add_task(async function test_multiple_init() {
    892  info("Initing TopSites multiple times should call _readDefaults only once.");
    893  let sandbox = sinon.createSandbox();
    894  sandbox.stub(NimbusFeatures.newtab, "onUpdate");
    895  sandbox.stub(TopSites, "_readDefaults");
    896  let cleanup = stubTopSites(sandbox);
    897 
    898  Assert.ok(TopSites._readDefaults.notCalled, "Read defaults not called.");
    899  for (let i = 0; i < 5; ++i) {
    900    await TopSites.init();
    901  }
    902  Assert.ok(TopSites._readDefaults.calledOnce, "Read defaults called once.");
    903 
    904  sandbox.restore();
    905  await cleanup();
    906 });
    907 
    908 add_task(async function test_multiple_init_delay() {
    909  TopSites.uninit();
    910 
    911  info(
    912    "Initing TopSites multiple times should allow callers " +
    913      "only call readDefaults once and wait until its finished."
    914  );
    915  let sandbox = sinon.createSandbox();
    916 
    917  let resolvePromise;
    918  let promise = new Promise(resolve => {
    919    resolvePromise = resolve;
    920  });
    921 
    922  sandbox.stub(NimbusFeatures.newtab, "onUpdate");
    923  sandbox.stub(TopSites, "_readDefaults").returns(promise);
    924  let cleanup = stubTopSites(sandbox);
    925 
    926  Assert.ok(TopSites._readDefaults.notCalled, "Read defaults not called.");
    927  let finishedPromiseCount = 0;
    928  let promises = [];
    929  let callInit = async () => {
    930    await TopSites.init();
    931    ++finishedPromiseCount;
    932  };
    933  for (let i = 0; i < 5; ++i) {
    934    promises.push(callInit());
    935  }
    936  Assert.equal(
    937    finishedPromiseCount,
    938    0,
    939    "Finished promise count should be equal."
    940  );
    941  Assert.ok(TopSites._readDefaults.calledOnce, "Read defaults called once.");
    942 
    943  info("Resolve the promises.");
    944  resolvePromise();
    945  await Promise.all(promises);
    946  Assert.equal(
    947    finishedPromiseCount,
    948    5,
    949    "Finished promise count should be equal."
    950  );
    951  Assert.ok(
    952    TopSites._readDefaults.calledOnce,
    953    "Read defaults was still only called once."
    954  );
    955 
    956  sandbox.restore();
    957  await cleanup();
    958 });
    959 
    960 add_task(async function test_uninit() {
    961  info("Un-initing TopSites should expire caches.");
    962  let sandbox = sinon.createSandbox();
    963 
    964  let cleanup = stubTopSites(sandbox);
    965  sandbox.stub(TopSites, "refresh");
    966  await TopSites.init();
    967 
    968  sandbox.stub(TopSites.pinnedCache, "expire");
    969  sandbox.stub(TopSites.frecentCache, "expire");
    970  TopSites.uninit();
    971 
    972  Assert.ok(
    973    TopSites.pinnedCache.expire.calledOnce,
    974    "pinnedCache.expire called once"
    975  );
    976  Assert.ok(
    977    TopSites.frecentCache.expire.calledOnce,
    978    "frecentCache.expire called once"
    979  );
    980 
    981  sandbox.restore();
    982  await cleanup();
    983 });
    984 
    985 add_task(async function test_get_sites_init() {
    986  info("TopSites.getSites should initialize TopSites if its not inited.");
    987  let sandbox = sinon.createSandbox();
    988 
    989  let cleanup = stubTopSites(sandbox);
    990  sandbox.stub(TopSites, "init");
    991 
    992  Assert.ok(TopSites.init.notCalled, "TopSites.init not called.");
    993  await TopSites.getSites();
    994  Assert.ok(TopSites.init.calledOnce, "TopSites.init called once.");
    995 
    996  sandbox.restore();
    997  await cleanup();
    998 });
    999 
   1000 add_task(async function test_get_sites_already_inited() {
   1001  info(
   1002    "TopSites.getSites should not call related initialization methods " +
   1003      "more than once if TopSites is already inited."
   1004  );
   1005  let sandbox = sinon.createSandbox();
   1006 
   1007  let cleanup = stubTopSites(sandbox);
   1008  sandbox.spy(TopSites, "_readDefaults");
   1009  await TopSites.init();
   1010 
   1011  Assert.ok(
   1012    TopSites._readDefaults.calledOnce,
   1013    "TopSites._readDefaults called once."
   1014  );
   1015  Assert.ok(
   1016    TopSites.updateCustomSearchShortcuts.calledOnce,
   1017    "TopSites.updateCustomSearchShortcuts called once."
   1018  );
   1019  await TopSites.getSites();
   1020  Assert.ok(
   1021    TopSites._readDefaults.calledOnce,
   1022    "TopSites._readDefaults still only called once."
   1023  );
   1024  Assert.ok(
   1025    TopSites.updateCustomSearchShortcuts.calledOnce,
   1026    "TopSites.updateCustomSearchShortcuts still only called once."
   1027  );
   1028 
   1029  sandbox.restore();
   1030  await cleanup();
   1031 });
   1032 
   1033 add_task(async function test_get_sites_delayed_init() {
   1034  info("TopSites.getSites should wait until initialization is done.");
   1035  let sandbox = sinon.createSandbox();
   1036 
   1037  let cleanup = stubTopSites(sandbox);
   1038 
   1039  // Ensure it's not initialized.
   1040  TopSites.uninit();
   1041 
   1042  let resolvePromise;
   1043  let promise = new Promise(resolve => {
   1044    resolvePromise = resolve;
   1045  });
   1046  sandbox.stub(TopSites, "init").returns(promise);
   1047 
   1048  let promises = [];
   1049  let finishedPromiseCount = 0;
   1050  let callGetSites = async () => {
   1051    await TopSites.getSites();
   1052    finishedPromiseCount += 1;
   1053  };
   1054  for (let i = 0; i < 5; ++i) {
   1055    promises.push(callGetSites());
   1056  }
   1057 
   1058  Assert.equal(
   1059    finishedPromiseCount,
   1060    0,
   1061    "All calls to TopSites.getSites() haven't finished."
   1062  );
   1063  resolvePromise();
   1064  await Promise.all(promises);
   1065  Assert.equal(
   1066    finishedPromiseCount,
   1067    5,
   1068    "All calls to TopSites.getSites() finished."
   1069  );
   1070 
   1071  sandbox.restore();
   1072  await cleanup();
   1073 });
   1074 
   1075 add_task(async function test_refresh() {
   1076  let sandbox = sinon.createSandbox();
   1077 
   1078  sandbox.stub(NimbusFeatures.newtab, "onUpdate");
   1079 
   1080  let cleanup = stubTopSites(sandbox);
   1081 
   1082  sandbox.stub(TopSites, "_fetchIcon");
   1083  TopSites._startedUp = true;
   1084 
   1085  info("TopSites.refresh should wait for tippytop to initialize");
   1086  TopSites._tippyTopProvider.initialized = false;
   1087  sandbox.stub(TopSites._tippyTopProvider, "init").resolves();
   1088 
   1089  await TopSites.refresh();
   1090 
   1091  Assert.ok(
   1092    TopSites._tippyTopProvider.init.calledOnce,
   1093    "TopSites._tippyTopProvider.init called once"
   1094  );
   1095 
   1096  info(
   1097    "TopSites.refresh should not init the tippyTopProvider if already initialized"
   1098  );
   1099  TopSites._tippyTopProvider.initialized = true;
   1100  TopSites._tippyTopProvider.init.resetHistory();
   1101 
   1102  await TopSites.refresh();
   1103 
   1104  Assert.ok(
   1105    TopSites._tippyTopProvider.init.notCalled,
   1106    "tippyTopProvider not initted again"
   1107  );
   1108 
   1109  sandbox.restore();
   1110  await cleanup();
   1111 });
   1112 
   1113 add_task(async function test_refresh_updateTopSites() {
   1114  // Ensure that TopSites isn't already initialized.
   1115  TopSites.uninit();
   1116 
   1117  let sandbox = sinon.createSandbox();
   1118  let cleanup = stubTopSites(sandbox);
   1119 
   1120  await TopSites.init();
   1121  TopSites._reset();
   1122 
   1123  // Clear the internal store.
   1124  TopSites._reset();
   1125 
   1126  let sites = await TopSites.getSites();
   1127  Assert.equal(sites.length, 0, "Sites is empty.");
   1128 
   1129  info("TopSites.refresh should update TopSites.sites");
   1130  let promise = TestUtils.topicObserved("topsites-refreshed");
   1131  // TODO: On New Tab, subscribe to updates to Top Sites.
   1132  await TopSites.refresh({ isStartup: true });
   1133  await promise;
   1134 
   1135  sites = await TopSites.getSites();
   1136  Assert.ok(sites.length, "Sites has values.");
   1137 
   1138  sandbox.restore();
   1139  await cleanup();
   1140 });
   1141 
   1142 add_task(async function test_refresh_dispatch() {
   1143  let sandbox = sinon.createSandbox();
   1144 
   1145  info("TopSites.refresh should dispatch an action with the links returned");
   1146 
   1147  let cleanup = stubTopSites(sandbox);
   1148  sandbox.stub(TopSites, "_fetchIcon");
   1149  TopSites._startedUp = true;
   1150 
   1151  await TopSites.refresh();
   1152  let reference = FAKE_LINKS.map(site =>
   1153    Object.assign({}, site, {
   1154      hostname: NewTabUtils.shortURL(site),
   1155      typedBonus: true,
   1156    })
   1157  );
   1158 
   1159  // TODO: On New Tab, subscribe to updates to Top Sites.
   1160  let sites = await TopSites.getSites();
   1161  Assert.deepEqual(sites, reference, "Sites are updated.");
   1162 
   1163  sandbox.restore();
   1164  await cleanup();
   1165 });
   1166 
   1167 add_task(async function test_refresh_empty_slots() {
   1168  let sandbox = sinon.createSandbox();
   1169 
   1170  info(
   1171    "TopSites.refresh should handle empty slots in the resulting top sites array"
   1172  );
   1173 
   1174  let cleanup = stubTopSites(sandbox);
   1175  sandbox.stub(TopSites, "_fetchIcon");
   1176  TopSites._startedUp = true;
   1177 
   1178  gGetTopSitesStub.resolves([FAKE_LINKS[0]]);
   1179  sandbox
   1180    .stub(NewTabUtils.pinnedLinks, "links")
   1181    .get(() => [
   1182      null,
   1183      null,
   1184      FAKE_LINKS[1],
   1185      null,
   1186      null,
   1187      null,
   1188      null,
   1189      null,
   1190      FAKE_LINKS[2],
   1191    ]);
   1192  await TopSites.refresh();
   1193 
   1194  let reference = FAKE_LINKS.map(site =>
   1195    Object.assign({}, site, {
   1196      hostname: NewTabUtils.shortURL(site),
   1197      typedBonus: true,
   1198    })
   1199  );
   1200  const expected = [
   1201    reference[0],
   1202    null,
   1203    createExpectedPinnedLink(reference[1], 2),
   1204    null,
   1205    null,
   1206    null,
   1207    null,
   1208    null,
   1209    createExpectedPinnedLink(reference[2], 8),
   1210  ];
   1211 
   1212  // TODO: On New Tab, subscribe to updates to Top Sites.
   1213  let sites = await TopSites.getSites();
   1214  assertLinks(sites, expected);
   1215 
   1216  gGetTopSitesStub.resolves(FAKE_LINKS);
   1217  sandbox.restore();
   1218  await cleanup();
   1219 });
   1220 
   1221 add_task(async function test_insert_part_2() {
   1222  let sandbox = sinon.createSandbox();
   1223 
   1224  info(
   1225    "TopSites.handlePlacesEvents should call refresh without a target " +
   1226      "if we clear history."
   1227  );
   1228 
   1229  let cleanup = stubTopSites(sandbox);
   1230  sandbox.stub(TopSites, "refresh");
   1231  TopSites.refresh.resetHistory();
   1232  await PlacesUtils.history.clear();
   1233  Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
   1234 
   1235  TopSites.refresh.resetHistory();
   1236 
   1237  info(
   1238    "TopSites.handlePlacesEvents should call refresh without a target " +
   1239      "if we remove a Topsite from history"
   1240  );
   1241  let uri = Services.io.newURI("https://www.example.com/");
   1242  await PlacesTestUtils.addVisits({
   1243    uri,
   1244    transition: PlacesUtils.history.TRANSITION_TYPED,
   1245    visitDate: Date.now() * 1000,
   1246  });
   1247  Assert.ok(TopSites.refresh.notCalled, "TopSites.refresh not called");
   1248  await PlacesUtils.history.remove(uri);
   1249 
   1250  Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
   1251 
   1252  info("TopSites.handlePlacesEvents should call refresh on newtab-linkBlocked");
   1253  TopSites.refresh.resetHistory();
   1254  // The event dispatched in NewTabUtils when a link is blocked;
   1255  TopSites.observe(null, "newtab-linkBlocked", null);
   1256  Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
   1257 
   1258  info("TopSites should call refresh on bookmark-added");
   1259  TopSites.refresh.resetHistory();
   1260  let bookmark = await PlacesUtils.bookmarks.insert({
   1261    url: "https://bookmark.example.com",
   1262    title: "Bookmark 1",
   1263    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
   1264  });
   1265  Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
   1266 
   1267  info("TopSites should call refresh on bookmark-removed");
   1268  TopSites.refresh.resetHistory();
   1269  await PlacesUtils.bookmarks.remove(bookmark);
   1270  Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
   1271 
   1272  info(
   1273    "TopSites.insert should call pin with correct args " +
   1274      "without an index specified"
   1275  );
   1276  sandbox.stub(NewTabUtils.pinnedLinks, "pin");
   1277 
   1278  let addAction = {
   1279    data: { site: { url: "foo.bar", label: "foo" } },
   1280  };
   1281  TopSites.insert(addAction);
   1282  Assert.ok(
   1283    NewTabUtils.pinnedLinks.pin.calledOnce,
   1284    "NewTabUtils.pinnedLinks.pin called once"
   1285  );
   1286  Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(addAction.data.site, 0));
   1287 
   1288  info("TopSites.insert should call pin with correct args");
   1289  NewTabUtils.pinnedLinks.pin.resetHistory();
   1290  let dropAction = {
   1291    data: { site: { url: "foo.bar", label: "foo" }, index: 3 },
   1292  };
   1293  TopSites.insert(dropAction);
   1294  Assert.ok(
   1295    NewTabUtils.pinnedLinks.pin.calledOnce,
   1296    "NewTabUtils.pinnedLinks.pin called once"
   1297  );
   1298  Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(dropAction.data.site, 3));
   1299 
   1300  sandbox.restore();
   1301  await cleanup();
   1302 });
   1303 
   1304 add_task(async function test_insert_part_1() {
   1305  let sandbox = sinon.createSandbox();
   1306  sandbox.stub(NewTabUtils.pinnedLinks, "pin");
   1307  let cleanup = stubTopSites(sandbox);
   1308 
   1309  {
   1310    info(
   1311      "TopSites.insert should pin site in first slot of pinned list with " +
   1312        "empty first slot"
   1313    );
   1314 
   1315    sandbox
   1316      .stub(NewTabUtils.pinnedLinks, "links")
   1317      .get(() => [null, { url: "example.com" }]);
   1318    let site = { url: "foo.bar", label: "foo" };
   1319    await TopSites.insert({ data: { site } });
   1320    Assert.ok(
   1321      NewTabUtils.pinnedLinks.pin.calledOnce,
   1322      "NewTabUtils.pinnedLinks.pin called once"
   1323    );
   1324    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
   1325    NewTabUtils.pinnedLinks.pin.resetHistory();
   1326  }
   1327 
   1328  {
   1329    info(
   1330      "TopSites.insert should move a pinned site in first slot to the " +
   1331        "next slot: part 1"
   1332    );
   1333    let site1 = { url: "example.com" };
   1334    sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [site1]);
   1335    let site = { url: "foo.bar", label: "foo" };
   1336 
   1337    await TopSites.insert({ data: { site } });
   1338    Assert.ok(
   1339      NewTabUtils.pinnedLinks.pin.calledTwice,
   1340      "NewTabUtils.pinnedLinks.pin called twice"
   1341    );
   1342    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
   1343    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 1));
   1344    NewTabUtils.pinnedLinks.pin.resetHistory();
   1345  }
   1346 
   1347  {
   1348    info(
   1349      "TopSites.insert should move a pinned site in first slot to the " +
   1350        "next slot: part 2"
   1351    );
   1352    let site1 = { url: "example.com" };
   1353    let site2 = { url: "example.org" };
   1354    sandbox
   1355      .stub(NewTabUtils.pinnedLinks, "links")
   1356      .get(() => [site1, null, site2]);
   1357    let site = { url: "foo.bar", label: "foo" };
   1358    await TopSites.insert({ data: { site } });
   1359    Assert.ok(
   1360      NewTabUtils.pinnedLinks.pin.calledTwice,
   1361      "NewTabUtils.pinnedLinks.pin called twice"
   1362    );
   1363    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
   1364    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 1));
   1365    NewTabUtils.pinnedLinks.pin.resetHistory();
   1366  }
   1367 
   1368  sandbox.restore();
   1369  await cleanup();
   1370 });
   1371 
   1372 add_task(async function test_insert_part_2() {
   1373  let sandbox = sinon.createSandbox();
   1374  sandbox.stub(NewTabUtils.pinnedLinks, "pin");
   1375  let cleanup = stubTopSites(sandbox);
   1376 
   1377  {
   1378    info(
   1379      "TopSites.insert should unpin the last site if all slots are " +
   1380        "already pinned"
   1381    );
   1382    let site1 = { url: "example.com" };
   1383    let site2 = { url: "example.org" };
   1384    let site3 = { url: "example.net" };
   1385    let site4 = { url: "example.biz" };
   1386    let site5 = { url: "example.info" };
   1387    let site6 = { url: "example.news" };
   1388    let site7 = { url: "example.lol" };
   1389    let site8 = { url: "example.golf" };
   1390    sandbox
   1391      .stub(NewTabUtils.pinnedLinks, "links")
   1392      .get(() => [site1, site2, site3, site4, site5, site6, site7, site8]);
   1393    Services.prefs.setIntPref(
   1394      "browser.newtabpage.activity-stream.topSitesRows",
   1395      1
   1396    );
   1397    let site = { url: "foo.bar", label: "foo" };
   1398    await TopSites.insert({ data: { site } });
   1399    Assert.equal(
   1400      NewTabUtils.pinnedLinks.pin.callCount,
   1401      8,
   1402      "NewTabUtils.pinnedLinks.pin called 8 times"
   1403    );
   1404    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
   1405    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 1));
   1406    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site2, 2));
   1407    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site3, 3));
   1408    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site4, 4));
   1409    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site5, 5));
   1410    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site6, 6));
   1411    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site7, 7));
   1412    NewTabUtils.pinnedLinks.pin.resetHistory();
   1413    Services.prefs.clearUserPref(
   1414      "browser.newtabpage.activity-stream.topSitesRows"
   1415    );
   1416  }
   1417 
   1418  {
   1419    info("TopSites.insert should trigger refresh");
   1420    sandbox.stub(TopSites, "refresh");
   1421    let addAction = {
   1422      data: { site: { url: "foo.com" } },
   1423    };
   1424 
   1425    await TopSites.insert(addAction);
   1426 
   1427    Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
   1428  }
   1429 
   1430  {
   1431    info("TopSites.insert should correctly handle different index values");
   1432    let index = -1;
   1433    let site = { url: "foo.bar", label: "foo" };
   1434    let action = { data: { index, site } };
   1435 
   1436    await TopSites.insert(action);
   1437    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
   1438 
   1439    index = undefined;
   1440    await TopSites.insert(action);
   1441    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
   1442 
   1443    NewTabUtils.pinnedLinks.pin.resetHistory();
   1444  }
   1445 
   1446  sandbox.restore();
   1447  await cleanup();
   1448 });
   1449 
   1450 add_task(async function test_insert_part_3() {
   1451  let sandbox = sinon.createSandbox();
   1452  sandbox.stub(NewTabUtils.pinnedLinks, "pin");
   1453  let cleanup = stubTopSites(sandbox);
   1454 
   1455  {
   1456    info("TopSites.insert should pin site in specified slot that is free");
   1457    sandbox
   1458      .stub(NewTabUtils.pinnedLinks, "links")
   1459      .get(() => [null, { url: "example.com" }]);
   1460 
   1461    let site = { url: "foo.bar", label: "foo" };
   1462 
   1463    await TopSites.insert({ data: { index: 2, site, draggedFromIndex: 0 } });
   1464    Assert.ok(
   1465      NewTabUtils.pinnedLinks.pin.calledOnce,
   1466      "NewTabUtils.pinnedLinks.pin called once"
   1467    );
   1468    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
   1469 
   1470    NewTabUtils.pinnedLinks.pin.resetHistory();
   1471  }
   1472 
   1473  {
   1474    info(
   1475      "TopSites.insert should move a pinned site in specified slot " +
   1476        "to the next slot"
   1477    );
   1478    sandbox
   1479      .stub(NewTabUtils.pinnedLinks, "links")
   1480      .get(() => [null, null, { url: "example.com" }]);
   1481 
   1482    let site = { url: "foo.bar", label: "foo" };
   1483 
   1484    await TopSites.insert({ data: { index: 2, site, draggedFromIndex: 3 } });
   1485    Assert.ok(
   1486      NewTabUtils.pinnedLinks.pin.calledTwice,
   1487      "NewTabUtils.pinnedLinks.pin called twice"
   1488    );
   1489    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
   1490    Assert.ok(
   1491      NewTabUtils.pinnedLinks.pin.calledWith({ url: "example.com" }, 3)
   1492    );
   1493 
   1494    NewTabUtils.pinnedLinks.pin.resetHistory();
   1495  }
   1496 
   1497  {
   1498    info(
   1499      "TopSites.insert should move pinned sites in the direction " +
   1500        "of the dragged site"
   1501    );
   1502 
   1503    let site1 = { url: "foo.bar", label: "foo" };
   1504    let site2 = { url: "example.com", label: "example" };
   1505    sandbox
   1506      .stub(NewTabUtils.pinnedLinks, "links")
   1507      .get(() => [null, null, site2]);
   1508 
   1509    await TopSites.insert({
   1510      data: { index: 2, site: site1, draggedFromIndex: 0 },
   1511    });
   1512    Assert.ok(
   1513      NewTabUtils.pinnedLinks.pin.calledTwice,
   1514      "NewTabUtils.pinnedLinks.pin called twice"
   1515    );
   1516    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 2));
   1517    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site2, 1));
   1518    NewTabUtils.pinnedLinks.pin.resetHistory();
   1519 
   1520    await TopSites.insert({
   1521      data: { index: 2, site: site1, draggedFromIndex: 5 },
   1522    });
   1523    Assert.ok(
   1524      NewTabUtils.pinnedLinks.pin.calledTwice,
   1525      "NewTabUtils.pinnedLinks.pin called twice"
   1526    );
   1527    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 2));
   1528    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site2, 3));
   1529    NewTabUtils.pinnedLinks.pin.resetHistory();
   1530  }
   1531 
   1532  {
   1533    info("TopSites.insert should not insert past the visible top sites");
   1534    let site1 = { url: "foo.bar", label: "foo" };
   1535    await TopSites.insert({
   1536      data: { index: 42, site: site1, draggedFromIndex: 0 },
   1537    });
   1538    Assert.ok(
   1539      NewTabUtils.pinnedLinks.pin.notCalled,
   1540      "NewTabUtils.pinnedLinks.pin wasn't called"
   1541    );
   1542 
   1543    NewTabUtils.pinnedLinks.pin.resetHistory();
   1544  }
   1545 
   1546  sandbox.restore();
   1547  await cleanup();
   1548 });
   1549 
   1550 add_task(async function test_pin_part_1() {
   1551  let sandbox = sinon.createSandbox();
   1552  sandbox.stub(NewTabUtils.pinnedLinks, "pin");
   1553  sandbox.spy(TopSites.pinnedCache, "request");
   1554  let cleanup = stubTopSites(sandbox);
   1555 
   1556  {
   1557    info("TopSites.pin should pin site in specified slot empty pinned list");
   1558    let site = {
   1559      url: "foo.bar",
   1560      label: "foo",
   1561    };
   1562    Assert.ok(
   1563      TopSites.pinnedCache.request.notCalled,
   1564      "TopSites.pinnedCache.request not called"
   1565    );
   1566    await TopSites.pin({ data: { index: 2, site } });
   1567    Assert.ok(
   1568      NewTabUtils.pinnedLinks.pin.called,
   1569      "NewTabUtils.pinnedLinks.pin called"
   1570    );
   1571    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
   1572    NewTabUtils.pinnedLinks.pin.resetHistory();
   1573    TopSites.pinnedCache.request.resetHistory();
   1574  }
   1575 
   1576  {
   1577    info(
   1578      "TopSites.pin should not do a link object lookup if custom " +
   1579        "screenshot field is not set"
   1580    );
   1581    let site = { url: "foo.bar", label: "foo" };
   1582    await TopSites.pin({ data: { index: 2, site } });
   1583    Assert.ok(
   1584      !TopSites.pinnedCache.request.called,
   1585      "TopSites.pinnedCache.request never called"
   1586    );
   1587    NewTabUtils.pinnedLinks.pin.resetHistory();
   1588    TopSites.pinnedCache.request.resetHistory();
   1589  }
   1590 
   1591  {
   1592    info(
   1593      "TopSites.pin should pin site in specified slot of pinned " +
   1594        "list that is free"
   1595    );
   1596    sandbox
   1597      .stub(NewTabUtils.pinnedLinks, "links")
   1598      .get(() => [null, { url: "example.com" }]);
   1599 
   1600    let site = { url: "foo.bar", label: "foo" };
   1601    await TopSites.pin({ data: { index: 2, site } });
   1602    Assert.ok(
   1603      NewTabUtils.pinnedLinks.pin.calledOnce,
   1604      "NewTabUtils.pinnedLinks.pin called once"
   1605    );
   1606    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
   1607    NewTabUtils.pinnedLinks.pin.resetHistory();
   1608  }
   1609 
   1610  sandbox.restore();
   1611  await cleanup();
   1612 });
   1613 
   1614 add_task(async function test_pin_part_2() {
   1615  let sandbox = sinon.createSandbox();
   1616  sandbox.stub(NewTabUtils.pinnedLinks, "pin");
   1617 
   1618  {
   1619    info("TopSites.pin should save the searchTopSite attribute if set");
   1620    sandbox
   1621      .stub(NewTabUtils.pinnedLinks, "links")
   1622      .get(() => [null, { url: "example.com" }]);
   1623 
   1624    let site = { url: "foo.bar", label: "foo", searchTopSite: true };
   1625    let cleanup = stubTopSites(sandbox);
   1626    await TopSites.pin({ data: { index: 2, site } });
   1627    Assert.ok(
   1628      NewTabUtils.pinnedLinks.pin.calledOnce,
   1629      "NewTabUtils.pinnedLinks.pin called once"
   1630    );
   1631    Assert.ok(NewTabUtils.pinnedLinks.pin.firstCall.args[0].searchTopSite);
   1632    NewTabUtils.pinnedLinks.pin.resetHistory();
   1633    await cleanup();
   1634  }
   1635 
   1636  {
   1637    info(
   1638      "TopSites.pin should NOT move a pinned site in specified " +
   1639        "slot to the next slot"
   1640    );
   1641    sandbox
   1642      .stub(NewTabUtils.pinnedLinks, "links")
   1643      .get(() => [null, null, { url: "example.com" }]);
   1644 
   1645    let site = { url: "foo.bar", label: "foo" };
   1646    let cleanup = stubTopSites(sandbox);
   1647    await TopSites.pin({ data: { index: 2, site } });
   1648    Assert.ok(
   1649      NewTabUtils.pinnedLinks.pin.calledOnce,
   1650      "NewTabUtils.pinnedLinks.pin called once"
   1651    );
   1652    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
   1653    NewTabUtils.pinnedLinks.pin.resetHistory();
   1654    await cleanup();
   1655  }
   1656 
   1657  {
   1658    info(
   1659      "TopSites.pin should not update LinksCache object with screenshot data" +
   1660        "properties between migrations"
   1661    );
   1662    sandbox
   1663      .stub(NewTabUtils.pinnedLinks, "links")
   1664      .get(() => [{ url: "https://foo.com/" }]);
   1665 
   1666    let cleanup = stubTopSites(sandbox);
   1667    let pinnedLinks = await TopSites.pinnedCache.request();
   1668    Assert.equal(pinnedLinks.length, 1);
   1669    TopSites.pinnedCache.expire();
   1670 
   1671    pinnedLinks[0].__sharedCache.updateLink("screenshot", "foo");
   1672 
   1673    pinnedLinks = await TopSites.pinnedCache.request();
   1674    Assert.equal(pinnedLinks[0].screenshot, undefined);
   1675 
   1676    // Force cache expiration in order to trigger a migration of objects
   1677    TopSites.pinnedCache.expire();
   1678    pinnedLinks[0].__sharedCache.updateLink("screenshot", "bar");
   1679 
   1680    pinnedLinks = await TopSites.pinnedCache.request();
   1681    Assert.equal(pinnedLinks[0].screenshot, undefined);
   1682    await cleanup();
   1683  }
   1684 
   1685  sandbox.restore();
   1686 });
   1687 
   1688 add_task(async function test_pin_part_3() {
   1689  let sandbox = sinon.createSandbox();
   1690  sandbox.stub(NewTabUtils.pinnedLinks, "pin");
   1691  sandbox.spy(TopSites, "insert");
   1692 
   1693  {
   1694    info("TopSites.pin should call insert if index < 0");
   1695    let site = { url: "foo.bar", label: "foo" };
   1696    let action = { data: { index: -1, site } };
   1697    let cleanup = stubTopSites(sandbox);
   1698    await TopSites.pin(action);
   1699 
   1700    Assert.ok(TopSites.insert.calledOnce, "TopSites.insert called once");
   1701    Assert.ok(TopSites.insert.calledWithExactly(action));
   1702    NewTabUtils.pinnedLinks.pin.resetHistory();
   1703    TopSites.insert.resetHistory();
   1704    await cleanup();
   1705  }
   1706 
   1707  {
   1708    info("TopSites.pin should not call insert if index == 0");
   1709    let site = { url: "foo.bar", label: "foo" };
   1710    let action = { data: { index: 0, site } };
   1711    let cleanup = stubTopSites(sandbox);
   1712    await TopSites.pin(action);
   1713 
   1714    Assert.ok(!TopSites.insert.called, "TopSites.insert not called");
   1715    NewTabUtils.pinnedLinks.pin.resetHistory();
   1716    await cleanup();
   1717  }
   1718 
   1719  {
   1720    info("TopSites.pin should trigger refresh");
   1721    let cleanup = stubTopSites(sandbox);
   1722    sandbox.stub(TopSites, "refresh");
   1723    let pinExistingAction = {
   1724      data: { site: FAKE_LINKS[4], index: 4 },
   1725    };
   1726 
   1727    await TopSites.pin(pinExistingAction);
   1728 
   1729    Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
   1730    NewTabUtils.pinnedLinks.pin.resetHistory();
   1731    await cleanup();
   1732  }
   1733 
   1734  sandbox.restore();
   1735 });
   1736 
   1737 add_task(async function test_pin_part_4() {
   1738  let sandbox = sinon.createSandbox();
   1739  let cleanup = stubTopSites(sandbox);
   1740 
   1741  info("TopSites.pin should call with correct parameters");
   1742  sandbox.stub(NewTabUtils.pinnedLinks, "pin");
   1743  sandbox.spy(TopSites, "pin");
   1744 
   1745  let pinAction = {
   1746    data: { site: { url: "foo.com" }, index: 7 },
   1747  };
   1748  await TopSites.pin(pinAction);
   1749  Assert.ok(
   1750    NewTabUtils.pinnedLinks.pin.calledOnce,
   1751    "NewTabUtils.pinnedLinks.pin called once"
   1752  );
   1753  Assert.ok(
   1754    NewTabUtils.pinnedLinks.pin.calledWithExactly(
   1755      pinAction.data.site,
   1756      pinAction.data.index
   1757    )
   1758  );
   1759  Assert.ok(
   1760    TopSites.pin.calledOnce,
   1761    "TopSites.pin should have been called once"
   1762  );
   1763 
   1764  info(
   1765    "TopSites.pin should unblock a previously blocked top site if " +
   1766      "we are now adding it manually via 'Add a Top Site' option"
   1767  );
   1768  sandbox.stub(NewTabUtils.blockedLinks, "unblock");
   1769  pinAction = {
   1770    data: { site: { url: "foo.com" }, index: -1 },
   1771  };
   1772  await TopSites.pin(pinAction);
   1773  Assert.ok(
   1774    NewTabUtils.blockedLinks.unblock.calledWith({
   1775      url: pinAction.data.site.url,
   1776    })
   1777  );
   1778 
   1779  info("TopSites.pin should call insert");
   1780  sandbox.stub(TopSites, "insert");
   1781  let addAction = {
   1782    data: { site: { url: "foo.com" } },
   1783  };
   1784 
   1785  await TopSites.pin(addAction);
   1786  Assert.ok(TopSites.insert.calledOnce, "TopSites.insert called once");
   1787 
   1788  info("TopSites.unpin should call unpin with correct parameters");
   1789 
   1790  sandbox
   1791    .stub(NewTabUtils.pinnedLinks, "links")
   1792    .get(() => [
   1793      null,
   1794      null,
   1795      { url: "foo.com" },
   1796      null,
   1797      null,
   1798      null,
   1799      null,
   1800      null,
   1801      FAKE_LINKS[0],
   1802    ]);
   1803  sandbox.stub(NewTabUtils.pinnedLinks, "unpin");
   1804 
   1805  let unpinAction = {
   1806    data: { site: { url: "foo.com" } },
   1807  };
   1808  await TopSites.unpin(unpinAction);
   1809  Assert.ok(
   1810    NewTabUtils.pinnedLinks.unpin.calledOnce,
   1811    "NewTabUtils.pinnedLinks.unpin called once"
   1812  );
   1813  Assert.ok(NewTabUtils.pinnedLinks.unpin.calledWith(unpinAction.data.site));
   1814 
   1815  sandbox.restore();
   1816  await cleanup();
   1817 });
   1818 
   1819 add_task(async function test_integration() {
   1820  let sandbox = sinon.createSandbox();
   1821 
   1822  info("Test adding a pinned site and removing it with actions");
   1823  let cleanup = stubTopSites(sandbox);
   1824 
   1825  TopSites._startedUp = true;
   1826 
   1827  TopSites._requestRichIcon = sandbox.stub();
   1828  let url = "https://pin.me";
   1829  sandbox.stub(NewTabUtils.pinnedLinks, "pin").callsFake(link => {
   1830    NewTabUtils.pinnedLinks.links.push(link);
   1831  });
   1832 
   1833  await TopSites.insert({ data: { site: { url } } });
   1834  await TestUtils.topicObserved("topsites-refreshed");
   1835  let oldSites = await TopSites.getSites();
   1836  NewTabUtils.pinnedLinks.links.pop();
   1837  // The event dispatched in NewTabUtils when a link is blocked;
   1838  TopSites.observe(null, "newtab-linkBlocked", null);
   1839  await TestUtils.topicObserved("topsites-refreshed");
   1840  let newSites = await TopSites.getSites();
   1841 
   1842  Assert.equal(oldSites[0].url, url, "Url matches.");
   1843  Assert.equal(newSites[0].url, FAKE_LINKS[0].url, "Url matches.");
   1844 
   1845  sandbox.restore();
   1846  await cleanup();
   1847 });
   1848 
   1849 add_task(async function test_improvesearch_noDefaultSearchTile_experiment() {
   1850  let sandbox = sinon.createSandbox();
   1851 
   1852  sandbox.stub(SearchService.prototype, "getDefault").resolves({
   1853    identifier: "google",
   1854  });
   1855 
   1856  {
   1857    info(
   1858      "TopSites.getLinksWithDefaults should filter out alexa top 5 " +
   1859        "search from the default sites"
   1860    );
   1861    let cleanup = stubTopSites(sandbox);
   1862    Services.prefs.setBoolPref(
   1863      "browser.newtabpage.activity-stream.improvesearch.noDefaultSearchTile",
   1864      true
   1865    );
   1866    let top5Test = [
   1867      "https://google.com",
   1868      "https://search.yahoo.com",
   1869      "https://yahoo.com",
   1870      "https://bing.com",
   1871      "https://ask.com",
   1872      "https://duckduckgo.com",
   1873    ];
   1874 
   1875    gGetTopSitesStub.resolves([
   1876      { url: "https://amazon.com" },
   1877      ...top5Test.map(url => ({ url })),
   1878    ]);
   1879 
   1880    const urlsReturned = (await TopSites.getLinksWithDefaults()).map(
   1881      link => link.url
   1882    );
   1883    Assert.ok(
   1884      urlsReturned.includes("https://amazon.com"),
   1885      "amazon included in default links"
   1886    );
   1887    top5Test.forEach(url =>
   1888      Assert.ok(!urlsReturned.includes(url), `Should not include ${url}`)
   1889    );
   1890 
   1891    Services.prefs.clearUserPref(
   1892      "browser.newtabpage.activity-stream.improvesearch.noDefaultSearchTile"
   1893    );
   1894    gGetTopSitesStub.resolves(FAKE_LINKS);
   1895    await cleanup();
   1896  }
   1897 
   1898  {
   1899    info(
   1900      "TopSites.getLinksWithDefaults should not filter out alexa, default " +
   1901        "search from the query results if the experiment pref is off"
   1902    );
   1903    let cleanup = stubTopSites(sandbox);
   1904    Services.prefs.setBoolPref(
   1905      "browser.newtabpage.activity-stream.improvesearch.noDefaultSearchTile",
   1906      false
   1907    );
   1908    gGetTopSitesStub.resolves([
   1909      { url: "https://google.com" },
   1910      { url: "https://foo.com" },
   1911      { url: "https://duckduckgo" },
   1912    ]);
   1913    let urlsReturned = (await TopSites.getLinksWithDefaults()).map(
   1914      link => link.url
   1915    );
   1916 
   1917    Assert.ok(urlsReturned.includes("https://google.com"));
   1918    gGetTopSitesStub.resolves(FAKE_LINKS);
   1919    Services.prefs.clearUserPref(
   1920      "browser.newtabpage.activity-stream.improvesearch.noDefaultSearchTile"
   1921    );
   1922    await cleanup();
   1923  }
   1924 
   1925  {
   1926    info(
   1927      "TopSites.getLinksWithDefaults should filter out the current " +
   1928        "default search from the default sites"
   1929    );
   1930    let cleanup = stubTopSites(sandbox);
   1931    Services.prefs.setBoolPref(
   1932      "browser.newtabpage.activity-stream.improvesearch.noDefaultSearchTile",
   1933      true
   1934    );
   1935 
   1936    sandbox.stub(TopSites, "_currentSearchHostname").get(() => "amazon");
   1937    Services.prefs.setStringPref(
   1938      "browser.newtabpage.activity-stream.default.sites",
   1939      "https://google.com,https://amazon.com"
   1940    );
   1941    gGetTopSitesStub.resolves([{ url: "https://foo.com" }]);
   1942 
   1943    let urlsReturned = (await TopSites.getLinksWithDefaults()).map(
   1944      link => link.url
   1945    );
   1946    Assert.ok(!urlsReturned.includes("https://amazon.com"));
   1947 
   1948    Services.prefs.clearUserPref(
   1949      "browser.newtabpage.activity-stream.improvesearch.noDefaultSearchTile"
   1950    );
   1951    Services.prefs.clearUserPref(
   1952      "browser.newtabpage.activity-stream.default.sites"
   1953    );
   1954    gGetTopSitesStub.resolves(FAKE_LINKS);
   1955    await cleanup();
   1956  }
   1957 
   1958  {
   1959    info(
   1960      "TopSites.getLinksWithDefaults should not filter out current " +
   1961        "default search from pinned sites even if it matches the current " +
   1962        "default search"
   1963    );
   1964    let cleanup = stubTopSites(sandbox);
   1965    Services.prefs.setBoolPref(
   1966      "browser.newtabpage.activity-stream.improvesearch.noDefaultSearchTile",
   1967      true
   1968    );
   1969 
   1970    sandbox
   1971      .stub(NewTabUtils.pinnedLinks, "links")
   1972      .get(() => [{ url: "google.com" }]);
   1973    gGetTopSitesStub.resolves([{ url: "https://foo.com" }]);
   1974 
   1975    let urlsReturned = (await TopSites.getLinksWithDefaults()).map(
   1976      link => link.url
   1977    );
   1978    Assert.ok(urlsReturned.includes("google.com"));
   1979 
   1980    Services.prefs.clearUserPref(
   1981      "browser.newtabpage.activity-stream.improvesearch.noDefaultSearchTile"
   1982    );
   1983    gGetTopSitesStub.resolves(FAKE_LINKS);
   1984    await cleanup();
   1985  }
   1986 
   1987  sandbox.restore();
   1988 });
   1989 
   1990 add_task(
   1991  async function test_improvesearch_noDefaultSearchTile_experiment_part_2() {
   1992    let sandbox = sinon.createSandbox();
   1993 
   1994    sandbox.stub(SearchService.prototype, "getDefault").resolves({
   1995      identifier: "google",
   1996    });
   1997 
   1998    sandbox.stub(TopSites, "refresh");
   1999 
   2000    {
   2001      info(
   2002        "TopSites.getLinksWithDefaults should call refresh and set " +
   2003          "._currentSearchHostname to the new engine hostname when the " +
   2004          "default search engine has been set"
   2005      );
   2006      let cleanup = stubTopSites(sandbox);
   2007      Services.prefs.setBoolPref(
   2008        "browser.newtabpage.activity-stream.improvesearch.noDefaultSearchTile",
   2009        true
   2010      );
   2011 
   2012      TopSites.observe(
   2013        null,
   2014        "browser-search-engine-modified",
   2015        "engine-default"
   2016      );
   2017      Assert.equal(TopSites._currentSearchHostname, "duckduckgo");
   2018      // Refresh is called twice:
   2019      // 1) For the change of `noDefaultSearchTile`
   2020      // 2) Default search engine changed "browser-search-engine-modified"
   2021      Assert.ok(TopSites.refresh.calledTwice, "TopSites.refresh called twice");
   2022 
   2023      Services.prefs.clearUserPref(
   2024        "browser.newtabpage.activity-stream.improvesearch.noDefaultSearchTile"
   2025      );
   2026      gGetTopSitesStub.resolves(FAKE_LINKS);
   2027      TopSites.refresh.resetHistory();
   2028      await cleanup();
   2029    }
   2030 
   2031    {
   2032      info(
   2033        "TopSites.getLinksWithDefaults should call refresh when the " +
   2034          "experiment pref has changed"
   2035      );
   2036      let cleanup = stubTopSites(sandbox);
   2037      Services.prefs.setBoolPref(
   2038        "browser.newtabpage.activity-stream.improvesearch.noDefaultSearchTile",
   2039        true
   2040      );
   2041      Assert.ok(
   2042        TopSites.refresh.calledOnce,
   2043        "TopSites.refresh was called once"
   2044      );
   2045 
   2046      Services.prefs.setBoolPref(
   2047        "browser.newtabpage.activity-stream.improvesearch.noDefaultSearchTile",
   2048        false
   2049      );
   2050      Assert.ok(
   2051        TopSites.refresh.calledTwice,
   2052        "TopSites.refresh was called twice"
   2053      );
   2054 
   2055      Services.prefs.clearUserPref(
   2056        "browser.newtabpage.activity-stream.improvesearch.noDefaultSearchTile"
   2057      );
   2058      gGetTopSitesStub.resolves(FAKE_LINKS);
   2059      TopSites.refresh.resetHistory();
   2060      await cleanup();
   2061    }
   2062 
   2063    sandbox.restore();
   2064  }
   2065 );
   2066 
   2067 // eslint-disable-next-line max-statements
   2068 add_task(async function test_improvesearch_topSitesSearchShortcuts() {
   2069  let sandbox = sinon.createSandbox();
   2070  let searchEngines = [{ aliases: ["@google"] }, { aliases: ["@amazon"] }];
   2071  sandbox
   2072    .stub(SearchService.prototype, "getAppProvidedEngines")
   2073    .resolves(searchEngines);
   2074  sandbox.stub(NewTabUtils.pinnedLinks, "pin").callsFake((site, index) => {
   2075    NewTabUtils.pinnedLinks.links[index] = site;
   2076  });
   2077 
   2078  let prepTopSites = () => {
   2079    Services.prefs.setBoolPref(
   2080      "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts",
   2081      true
   2082    );
   2083    Services.prefs.setStringPref(
   2084      "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts.searchEngines",
   2085      "google,amazon"
   2086    );
   2087    Services.prefs.setStringPref(
   2088      "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts.havePinned",
   2089      ""
   2090    );
   2091  };
   2092 
   2093  {
   2094    info(
   2095      "TopSites should updateCustomSearchShortcuts when experiment " +
   2096        "pref is turned on"
   2097    );
   2098    let cleanup = stubTopSites(sandbox);
   2099    prepTopSites();
   2100    Services.prefs.setBoolPref(
   2101      "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts",
   2102      false
   2103    );
   2104    // stubTopSites stubs updateCustomSearchShortcuts, when we need to add
   2105    // a spy.
   2106    TopSites.updateCustomSearchShortcuts.restore();
   2107    sandbox.spy(TopSites, "updateCustomSearchShortcuts");
   2108 
   2109    // turn the experiment on
   2110    Services.prefs.setBoolPref(
   2111      "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts",
   2112      true
   2113    );
   2114 
   2115    Assert.ok(
   2116      TopSites.updateCustomSearchShortcuts.calledOnce,
   2117      "TopSites.updateCustomSearchShortcuts called once"
   2118    );
   2119    Services.prefs.clearUserPref(
   2120      "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts"
   2121    );
   2122    TopSites.updateCustomSearchShortcuts.restore();
   2123    await cleanup();
   2124  }
   2125 
   2126  {
   2127    info(
   2128      "TopSites should filter out default top sites that match a " +
   2129        "hostname of a search shortcut if previously blocked"
   2130    );
   2131    let cleanup = stubTopSites(sandbox);
   2132    prepTopSites();
   2133    TopSites.refreshDefaults("https://amazon.ca");
   2134    sandbox
   2135      .stub(NewTabUtils.blockedLinks, "links")
   2136      .value([{ url: "https://amazon.com" }]);
   2137    sandbox.stub(NewTabUtils.blockedLinks, "isBlocked").callsFake(site => {
   2138      return NewTabUtils.blockedLinks.links[0].url === site.url;
   2139    });
   2140 
   2141    let urlsReturned = (await TopSites.getLinksWithDefaults()).map(
   2142      link => link.url
   2143    );
   2144    Assert.ok(!urlsReturned.includes("https://amazon.ca"));
   2145    await cleanup();
   2146  }
   2147 
   2148  {
   2149    info("TopSites should update frecent search topsite icon");
   2150    let cleanup = stubTopSites(sandbox);
   2151    prepTopSites();
   2152    sandbox.stub(TopSites._tippyTopProvider, "processSite").callsFake(site => {
   2153      site.tippyTopIcon = "icon.png";
   2154      site.backgroundColor = "#fff";
   2155      return site;
   2156    });
   2157    gGetTopSitesStub.resolves([{ url: "https://google.com" }]);
   2158 
   2159    let urlsReturned = await TopSites.getLinksWithDefaults();
   2160 
   2161    let defaultSearchTopsite = urlsReturned.find(
   2162      s => s.url === "https://google.com"
   2163    );
   2164    Assert.ok(defaultSearchTopsite.searchTopSite);
   2165    Assert.equal(defaultSearchTopsite.tippyTopIcon, "icon.png");
   2166    Assert.equal(defaultSearchTopsite.backgroundColor, "#fff");
   2167    gGetTopSitesStub.resolves(FAKE_LINKS);
   2168    TopSites._tippyTopProvider.processSite.restore();
   2169    await cleanup();
   2170  }
   2171 
   2172  {
   2173    info("TopSites should update default search topsite icon");
   2174    let cleanup = stubTopSites(sandbox);
   2175    prepTopSites();
   2176    sandbox.stub(TopSites._tippyTopProvider, "processSite").callsFake(site => {
   2177      site.tippyTopIcon = "icon.png";
   2178      site.backgroundColor = "#fff";
   2179      return site;
   2180    });
   2181    gGetTopSitesStub.resolves([{ url: "https://foo.com" }]);
   2182 
   2183    let urlsReturned = await TopSites.getLinksWithDefaults();
   2184 
   2185    let defaultSearchTopsite = urlsReturned.find(
   2186      s => s.url === "https://amazon.com"
   2187    );
   2188    Assert.ok(defaultSearchTopsite.searchTopSite);
   2189    Assert.equal(defaultSearchTopsite.tippyTopIcon, "icon.png");
   2190    Assert.equal(defaultSearchTopsite.backgroundColor, "#fff");
   2191    gGetTopSitesStub.resolves(FAKE_LINKS);
   2192    TopSites._tippyTopProvider.processSite.restore();
   2193    await cleanup();
   2194  }
   2195 
   2196  {
   2197    info(
   2198      "TopSites should dispatch UPDATE_SEARCH_SHORTCUTS on " +
   2199        "updateCustomSearchShortcuts"
   2200    );
   2201    let cleanup = stubTopSites(sandbox);
   2202    prepTopSites();
   2203    Services.prefs.setBoolPref(
   2204      "browser.newtabpage.activity-stream.improvesearch.noDefaultSearchTile",
   2205      true
   2206    );
   2207    // stubTopSites stubs updateCustomSearchShortcuts, when in this case, we
   2208    // want to check the effect of the method.
   2209    TopSites.updateCustomSearchShortcuts.restore();
   2210    await TopSites.updateCustomSearchShortcuts();
   2211    let searchShortcuts = await TopSites.getSearchShortcuts();
   2212    Assert.deepEqual(searchShortcuts, [
   2213      {
   2214        keyword: "@google",
   2215        shortURL: "google",
   2216        url: "https://google.com",
   2217        backgroundColor: undefined,
   2218        smallFavicon:
   2219          "chrome://activity-stream/content/data/content/tippytop/favicons/google-com.ico",
   2220        tippyTopIcon:
   2221          "chrome://activity-stream/content/data/content/tippytop/images/google-com@2x.png",
   2222      },
   2223      {
   2224        keyword: "@amazon",
   2225        shortURL: "amazon",
   2226        url: "https://amazon.com",
   2227        backgroundColor: undefined,
   2228        smallFavicon:
   2229          "chrome://activity-stream/content/data/content/tippytop/favicons/amazon.ico",
   2230        tippyTopIcon:
   2231          "chrome://activity-stream/content/data/content/tippytop/images/amazon@2x.png",
   2232      },
   2233    ]);
   2234    await cleanup();
   2235  }
   2236 
   2237  {
   2238    info(
   2239      "TopSites should refresh when top sites search shortcut feature gate pref changes."
   2240    );
   2241    let cleanup = stubTopSites(sandbox);
   2242    prepTopSites();
   2243 
   2244    let promise = TestUtils.topicObserved("topsites-refreshed");
   2245    Services.prefs.setBoolPref(
   2246      "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts",
   2247      false
   2248    );
   2249    await promise;
   2250 
   2251    let sites = await TopSites.getSites();
   2252    let searchTopSiteCount = sites.reduce(
   2253      (acc, current) => (current.searchTopSite ? 1 : 0 + acc),
   2254      0
   2255    );
   2256    Assert.equal(searchTopSiteCount, 0, "Number of search top sites.");
   2257 
   2258    promise = TestUtils.topicObserved("topsites-refreshed");
   2259    Services.prefs.setBoolPref(
   2260      "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts",
   2261      true
   2262    );
   2263    await promise;
   2264    sites = await TopSites.getSites();
   2265    searchTopSiteCount = sites.reduce(
   2266      (acc, current) => acc + (current.searchTopSite ? 1 : 0),
   2267      0
   2268    );
   2269    Assert.equal(searchTopSiteCount, 2, "Number of search top sites.");
   2270    await cleanup();
   2271  }
   2272 
   2273  Services.prefs.clearUserPref(
   2274    "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts"
   2275  );
   2276  Services.prefs.clearUserPref(
   2277    "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts.searchEngines"
   2278  );
   2279  Services.prefs.clearUserPref(
   2280    "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts.havePinned"
   2281  );
   2282  sandbox.restore();
   2283 });
   2284 
   2285 add_task(async function test_updatePinnedSearchShortcuts() {
   2286  let sandbox = sinon.createSandbox();
   2287  sandbox.stub(NewTabUtils.pinnedLinks, "pin");
   2288  sandbox.stub(NewTabUtils.pinnedLinks, "unpin");
   2289 
   2290  {
   2291    info(
   2292      "TopSites.updatePinnedSearchShortcuts should unpin a " +
   2293        "shortcut in deletedShortcuts"
   2294    );
   2295    let cleanup = stubTopSites(sandbox);
   2296 
   2297    let deletedShortcuts = [
   2298      {
   2299        url: "https://google.com",
   2300        searchVendor: "google",
   2301        label: "google",
   2302        searchTopSite: true,
   2303      },
   2304    ];
   2305    let addedShortcuts = [];
   2306    sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [
   2307      null,
   2308      null,
   2309      {
   2310        url: "https://amazon.com",
   2311        searchVendor: "amazon",
   2312        label: "amazon",
   2313        searchTopSite: true,
   2314      },
   2315    ]);
   2316    TopSites.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
   2317    Assert.ok(
   2318      NewTabUtils.pinnedLinks.pin.notCalled,
   2319      "NewTabUtils.pinnedLinks.pin not called"
   2320    );
   2321    Assert.ok(
   2322      NewTabUtils.pinnedLinks.unpin.calledOnce,
   2323      "NewTabUtils.pinnedLinks.unpin called once"
   2324    );
   2325    Assert.ok(
   2326      NewTabUtils.pinnedLinks.unpin.calledWith({
   2327        url: "https://google.com",
   2328      })
   2329    );
   2330 
   2331    NewTabUtils.pinnedLinks.pin.resetHistory();
   2332    NewTabUtils.pinnedLinks.unpin.resetHistory();
   2333    await cleanup();
   2334  }
   2335 
   2336  {
   2337    info(
   2338      "TopSites.updatePinnedSearchShortcuts should pin a shortcut " +
   2339        "in addedShortcuts"
   2340    );
   2341    let cleanup = stubTopSites(sandbox);
   2342 
   2343    let addedShortcuts = [
   2344      {
   2345        url: "https://google.com",
   2346        searchVendor: "google",
   2347        label: "google",
   2348        searchTopSite: true,
   2349      },
   2350    ];
   2351    let deletedShortcuts = [];
   2352    sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [
   2353      null,
   2354      null,
   2355      {
   2356        url: "https://amazon.com",
   2357        searchVendor: "amazon",
   2358        label: "amazon",
   2359        searchTopSite: true,
   2360      },
   2361    ]);
   2362    TopSites.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
   2363    Assert.ok(
   2364      NewTabUtils.pinnedLinks.unpin.notCalled,
   2365      "NewTabUtils.pinnedLinks.unpin not called"
   2366    );
   2367    Assert.ok(
   2368      NewTabUtils.pinnedLinks.pin.calledOnce,
   2369      "NewTabUtils.pinnedLinks.pin called once"
   2370    );
   2371    Assert.ok(
   2372      NewTabUtils.pinnedLinks.pin.calledWith(
   2373        {
   2374          label: "google",
   2375          searchTopSite: true,
   2376          searchVendor: "google",
   2377          url: "https://google.com",
   2378        },
   2379        0
   2380      )
   2381    );
   2382 
   2383    NewTabUtils.pinnedLinks.pin.resetHistory();
   2384    NewTabUtils.pinnedLinks.unpin.resetHistory();
   2385    await cleanup();
   2386  }
   2387 
   2388  {
   2389    info(
   2390      "TopSites.updatePinnedSearchShortcuts should pin and unpin " +
   2391        "in the same action"
   2392    );
   2393    let cleanup = stubTopSites(sandbox);
   2394 
   2395    let addedShortcuts = [
   2396      {
   2397        url: "https://google.com",
   2398        searchVendor: "google",
   2399        label: "google",
   2400        searchTopSite: true,
   2401      },
   2402      {
   2403        url: "https://ebay.com",
   2404        searchVendor: "ebay",
   2405        label: "ebay",
   2406        searchTopSite: true,
   2407      },
   2408    ];
   2409    let deletedShortcuts = [
   2410      {
   2411        url: "https://amazon.com",
   2412        searchVendor: "amazon",
   2413        label: "amazon",
   2414        searchTopSite: true,
   2415      },
   2416    ];
   2417 
   2418    sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [
   2419      { url: "https://foo.com" },
   2420      {
   2421        url: "https://amazon.com",
   2422        searchVendor: "amazon",
   2423        label: "amazon",
   2424        searchTopSite: true,
   2425      },
   2426    ]);
   2427    TopSites.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
   2428 
   2429    Assert.ok(
   2430      NewTabUtils.pinnedLinks.unpin.calledOnce,
   2431      "NewTabUtils.pinnedLinks.unpin called once"
   2432    );
   2433    Assert.ok(
   2434      NewTabUtils.pinnedLinks.pin.calledTwice,
   2435      "NewTabUtils.pinnedLinks.pin called twice"
   2436    );
   2437 
   2438    NewTabUtils.pinnedLinks.pin.resetHistory();
   2439    NewTabUtils.pinnedLinks.unpin.resetHistory();
   2440    await cleanup();
   2441  }
   2442 
   2443  {
   2444    info(
   2445      "TopSites.updatePinnedSearchShortcuts should pin a shortcut in " +
   2446        "addedShortcuts even if pinnedLinks is full"
   2447    );
   2448    let cleanup = stubTopSites(sandbox);
   2449 
   2450    let addedShortcuts = [
   2451      {
   2452        url: "https://google.com",
   2453        searchVendor: "google",
   2454        label: "google",
   2455        searchTopSite: true,
   2456      },
   2457    ];
   2458    let deletedShortcuts = [];
   2459    sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => FAKE_LINKS);
   2460    TopSites.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
   2461 
   2462    Assert.ok(
   2463      NewTabUtils.pinnedLinks.unpin.notCalled,
   2464      "NewTabUtils.pinnedLinks.unpin not called"
   2465    );
   2466    Assert.ok(
   2467      NewTabUtils.pinnedLinks.pin.calledWith(
   2468        { label: "google", searchTopSite: true, url: "https://google.com" },
   2469        0
   2470      ),
   2471      "NewTabUtils.pinnedLinks.unpin not called"
   2472    );
   2473 
   2474    NewTabUtils.pinnedLinks.pin.resetHistory();
   2475    NewTabUtils.pinnedLinks.unpin.resetHistory();
   2476    await cleanup();
   2477  }
   2478 
   2479  sandbox.restore();
   2480 });
   2481 
   2482 add_task(async function test_insertPinned() {
   2483  info("#insertPinned");
   2484 
   2485  function createLinks(count) {
   2486    return new Array(count).fill(null).map((v, i) => ({ url: `site${i}.com` }));
   2487  }
   2488 
   2489  info("should place pinned links where they belong");
   2490  {
   2491    let links = createLinks(12);
   2492    const pinned = [
   2493      { url: "http://github.com/mozilla/activity-stream", title: "moz/a-s" },
   2494      { url: "http://example.com", title: "example" },
   2495    ];
   2496 
   2497    const result = insertPinned(links, pinned);
   2498    for (let index of [0, 1]) {
   2499      Assert.equal(result[index].url, pinned[index].url, "Pinned URL matches");
   2500      Assert.ok(result[index].isPinned, "Link is marked as pinned");
   2501      Assert.equal(result[index].pinIndex, index, "Pin index is correct");
   2502    }
   2503    Assert.deepEqual(result.slice(2), links, "Remaining links are unchanged");
   2504  }
   2505 
   2506  info("should handle empty slots in the pinned list");
   2507  {
   2508    let links = createLinks(12);
   2509    const pinned = [
   2510      null,
   2511      { url: "http://github.com/mozilla/activity-stream", title: "moz/a-s" },
   2512      null,
   2513      null,
   2514      { url: "http://example.com", title: "example" },
   2515    ];
   2516 
   2517    const result = insertPinned(links, pinned);
   2518    for (let index of [1, 4]) {
   2519      Assert.equal(result[index].url, pinned[index].url, "Pinned URL matches");
   2520      Assert.ok(result[index].isPinned, "Link is marked as pinned");
   2521      Assert.equal(result[index].pinIndex, index, "Pin index is correct");
   2522    }
   2523    result.splice(4, 1);
   2524    result.splice(1, 1);
   2525    Assert.deepEqual(result, links, "Remaining links are unchanged");
   2526  }
   2527 
   2528  info("should handle a pinned site past the end of the list of links");
   2529  {
   2530    const pinned = [];
   2531    pinned[11] = {
   2532      url: "http://github.com/mozilla/activity-stream",
   2533      title: "moz/a-s",
   2534    };
   2535 
   2536    const result = insertPinned([], pinned);
   2537    Assert.equal(result[11].url, pinned[11].url, "Pinned URL matches");
   2538    Assert.ok(result[11].isPinned, "Link is marked as pinned");
   2539    Assert.equal(result[11].pinIndex, 11, "Pin index is correct");
   2540  }
   2541 
   2542  info("should unpin previously pinned links no longer in the pinned list");
   2543  {
   2544    let links = createLinks(12);
   2545    const pinned = [];
   2546    links[2].isPinned = true;
   2547    links[2].pinIndex = 2;
   2548 
   2549    const result = insertPinned(links, pinned);
   2550    Assert.ok(!result[2].isPinned, "isPinned property removed");
   2551    Assert.ok(!result[2].pinIndex, "pinIndex property removed");
   2552  }
   2553 
   2554  info("should handle a link present in both the links and pinned list");
   2555  {
   2556    let links = createLinks(12);
   2557    const pinned = [links[7]];
   2558 
   2559    const result = insertPinned(links, pinned);
   2560    Assert.equal(links.length, result.length, "Length of links is unchanged");
   2561  }
   2562 
   2563  info("should not modify the original data");
   2564  {
   2565    let links = createLinks(12);
   2566    const pinned = [{ url: "http://example.com" }];
   2567 
   2568    insertPinned(links, pinned);
   2569 
   2570    Assert.equal(
   2571      typeof pinned[0].isPinned,
   2572      "undefined",
   2573      "Pinned data is not mutated"
   2574    );
   2575  }
   2576 });