tor-browser

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

test_PlacesFeed.js (41164B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 ChromeUtils.defineESModuleGetters(this, {
      7  actionCreators: "resource://newtab/common/Actions.mjs",
      8  actionTypes: "resource://newtab/common/Actions.mjs",
      9  AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
     10  NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
     11  PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.sys.mjs",
     12  PlacesFeed: "resource://newtab/lib/PlacesFeed.sys.mjs",
     13  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     14  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     15  SearchService: "resource://gre/modules/SearchService.sys.mjs",
     16  sinon: "resource://testing-common/Sinon.sys.mjs",
     17  TestUtils: "resource://testing-common/TestUtils.sys.mjs",
     18 });
     19 
     20 const FAKE_BOOKMARK = {
     21  bookmarkGuid: "D3r1sKRobtbW",
     22  bookmarkTitle: "Foo",
     23  dateAdded: 123214232,
     24  url: "foo.com",
     25 };
     26 const TYPE_BOOKMARK = 1; // This is fake, for testing
     27 const SOURCES = {
     28  DEFAULT: 0,
     29  SYNC: 1,
     30  IMPORT: 2,
     31  RESTORE: 5,
     32  RESTORE_ON_STARTUP: 6,
     33 };
     34 
     35 // The event dispatched in NewTabUtils when a link is blocked;
     36 const BLOCKED_EVENT = "newtab-linkBlocked";
     37 
     38 const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors";
     39 
     40 function getPlacesFeedForTest(sandbox) {
     41  let feed = new PlacesFeed();
     42  feed.store = {
     43    dispatch: sandbox.spy(),
     44    feeds: {
     45      get: sandbox.stub(),
     46    },
     47  };
     48 
     49  sandbox.stub(AboutNewTab, "activityStream").value({
     50    store: feed.store,
     51  });
     52 
     53  return feed;
     54 }
     55 
     56 add_task(async function test_construction() {
     57  info("PlacesFeed construction should work");
     58  let feed = new PlacesFeed();
     59  Assert.ok(feed, "PlacesFeed could be constructed.");
     60 });
     61 
     62 add_task(async function test_PlacesObserver() {
     63  info("PlacesFeed should have a PlacesObserver that dispatches to the store");
     64  let sandbox = sinon.createSandbox();
     65  let feed = getPlacesFeedForTest(sandbox);
     66 
     67  let action = { type: "FOO" };
     68  feed.placesObserver.dispatch(action);
     69 
     70  await TestUtils.waitForTick();
     71  Assert.ok(feed.store.dispatch.calledOnce, "PlacesFeed.store dispatch called");
     72  Assert.equal(feed.store.dispatch.firstCall.args[0].type, action.type);
     73 
     74  sandbox.restore();
     75 });
     76 
     77 add_task(async function test_addToBlockedTopSitesSponsors_add_to_blocklist() {
     78  info(
     79    "PlacesFeed.addToBlockedTopSitesSponsors should add the blocked sponsors " +
     80      "to the blocklist"
     81  );
     82  let sandbox = sinon.createSandbox();
     83  let feed = getPlacesFeedForTest(sandbox);
     84  Services.prefs.setStringPref(
     85    TOP_SITES_BLOCKED_SPONSORS_PREF,
     86    `["foo","bar"]`
     87  );
     88 
     89  feed.addToBlockedTopSitesSponsors([
     90    { url: "test.com" },
     91    { url: "test1.com" },
     92  ]);
     93 
     94  let blockedSponsors = JSON.parse(
     95    Services.prefs.getStringPref(TOP_SITES_BLOCKED_SPONSORS_PREF)
     96  );
     97  Assert.deepEqual(
     98    new Set(["foo", "bar", "test", "test1"]),
     99    new Set(blockedSponsors)
    100  );
    101 
    102  Services.prefs.clearUserPref(TOP_SITES_BLOCKED_SPONSORS_PREF);
    103  sandbox.restore();
    104 });
    105 
    106 add_task(async function test_addToBlockedTopSitesSponsors_no_dupes() {
    107  info(
    108    "PlacesFeed.addToBlockedTopSitesSponsors should not add duplicate " +
    109      "sponsors to the blocklist"
    110  );
    111  let sandbox = sinon.createSandbox();
    112  let feed = getPlacesFeedForTest(sandbox);
    113  Services.prefs.setStringPref(
    114    TOP_SITES_BLOCKED_SPONSORS_PREF,
    115    `["foo","bar"]`
    116  );
    117 
    118  feed.addToBlockedTopSitesSponsors([
    119    { url: "foo.com" },
    120    { url: "bar.com" },
    121    { url: "test.com" },
    122  ]);
    123 
    124  let blockedSponsors = JSON.parse(
    125    Services.prefs.getStringPref(TOP_SITES_BLOCKED_SPONSORS_PREF)
    126  );
    127  Assert.deepEqual(new Set(["foo", "bar", "test"]), new Set(blockedSponsors));
    128 
    129  Services.prefs.clearUserPref(TOP_SITES_BLOCKED_SPONSORS_PREF);
    130  sandbox.restore();
    131 });
    132 
    133 add_task(async function test_onAction_PlacesEvents() {
    134  info(
    135    "PlacesFeed.onAction should add bookmark, history, places, blocked " +
    136      "observers on INIT"
    137  );
    138  let sandbox = sinon.createSandbox();
    139  let feed = getPlacesFeedForTest(sandbox);
    140  sandbox.stub(feed.placesObserver, "handlePlacesEvent");
    141 
    142  feed.onAction({ type: actionTypes.INIT });
    143  // The PlacesObserver registration happens at the next tick of the
    144  // event loop.
    145  await TestUtils.waitForTick();
    146 
    147  // These are some dummy PlacesEvents that we'll pass through the
    148  // PlacesObserver service, checking that the handlePlacesEvent receives them
    149  // properly.
    150  let notifications = [
    151    new PlacesBookmarkAddition({
    152      dateAdded: 0,
    153      guid: "dQFSYrbM5SJN",
    154      id: -1,
    155      index: 0,
    156      isTagging: false,
    157      itemType: 1,
    158      parentGuid: "n_HOEFys1qsL",
    159      parentId: -2,
    160      source: 0,
    161      title: "test-123",
    162      tags: "tags",
    163      url: "http://example.com/test-123",
    164      frecency: 0,
    165      hidden: false,
    166      visitCount: 0,
    167      lastVisitDate: 0,
    168      targetFolderGuid: null,
    169      targetFolderItemId: -1,
    170      targetFolderTitle: null,
    171    }),
    172    new PlacesBookmarkRemoved({
    173      id: -1,
    174      url: "http://example.com/test-123",
    175      title: "test-123",
    176      itemType: 1,
    177      parentId: -2,
    178      index: 0,
    179      guid: "M3WYgJlm2Jlx",
    180      parentGuid: "DO1f97R4KC3Y",
    181      source: 0,
    182      isTagging: false,
    183      isDescendantRemoval: false,
    184    }),
    185    new PlacesHistoryCleared(),
    186    new PlacesVisitRemoved({
    187      url: "http://example.com/test-123",
    188      pageGuid: "sPVcW2V4H7Rg",
    189      reason: PlacesVisitRemoved.REASON_DELETED,
    190      transitionType: 0,
    191      isRemovedFromStore: true,
    192      isPartialVisistsRemoval: false,
    193    }),
    194  ];
    195 
    196  for (let notification of notifications) {
    197    PlacesUtils.observers.notifyListeners([notification]);
    198    Assert.ok(
    199      feed.placesObserver.handlePlacesEvent.calledOnce,
    200      "PlacesFeed.handlePlacesEvent called"
    201    );
    202    Assert.ok(feed.placesObserver.handlePlacesEvent.calledWith([notification]));
    203    feed.placesObserver.handlePlacesEvent.resetHistory();
    204  }
    205 
    206  info(
    207    "PlacesFeed.onAction remove bookmark, history, places, blocked " +
    208      "observers, and timers on UNINIT"
    209  );
    210 
    211  let placesChangedTimerCancel = sandbox.spy();
    212  feed.placesChangedTimer = {
    213    cancel: placesChangedTimerCancel,
    214  };
    215 
    216  // Unlike INIT, UNINIT removes the observers synchronously, so no need to
    217  // wait for the event loop to tick around again.
    218  feed.onAction({ type: actionTypes.UNINIT });
    219 
    220  for (let notification of notifications) {
    221    PlacesUtils.observers.notifyListeners([notification]);
    222    Assert.ok(
    223      feed.placesObserver.handlePlacesEvent.notCalled,
    224      "PlacesFeed.handlePlacesEvent not called"
    225    );
    226    feed.placesObserver.handlePlacesEvent.resetHistory();
    227  }
    228 
    229  Assert.equal(feed.placesChangedTimer, null);
    230  Assert.ok(placesChangedTimerCancel.calledOnce);
    231 
    232  sandbox.restore();
    233 });
    234 
    235 add_task(async function test_onAction_BLOCK_URL() {
    236  info("PlacesFeed.onAction should block a url on BLOCK_URL");
    237  let sandbox = sinon.createSandbox();
    238  let feed = getPlacesFeedForTest(sandbox);
    239  sandbox.stub(NewTabUtils.activityStreamLinks, "blockURL");
    240 
    241  feed.onAction({
    242    type: actionTypes.BLOCK_URL,
    243    data: [{ url: "apple.com", pocket_id: 1234 }],
    244  });
    245  Assert.ok(
    246    NewTabUtils.activityStreamLinks.blockURL.calledWith({
    247      url: "apple.com",
    248      pocket_id: 1234,
    249    })
    250  );
    251 
    252  sandbox.restore();
    253 });
    254 
    255 add_task(async function test_onAction_BLOCK_URL_topsites_sponsors() {
    256  info(
    257    "PlacesFeed.onAction BLOCK_URL should update the blocked top " +
    258      "sites sponsors"
    259  );
    260  let sandbox = sinon.createSandbox();
    261  let feed = getPlacesFeedForTest(sandbox);
    262  sandbox.stub(feed, "addToBlockedTopSitesSponsors");
    263 
    264  feed.onAction({
    265    type: actionTypes.BLOCK_URL,
    266    data: [{ url: "foo.com", pocket_id: 1234, isSponsoredTopSite: 1 }],
    267  });
    268  Assert.ok(feed.addToBlockedTopSitesSponsors.calledWith([{ url: "foo.com" }]));
    269 
    270  sandbox.restore();
    271 });
    272 
    273 add_task(async function test_onAction_BOOKMARK_URL() {
    274  info("PlacesFeed.onAction should bookmark a url on BOOKMARK_URL");
    275  let sandbox = sinon.createSandbox();
    276  let feed = getPlacesFeedForTest(sandbox);
    277  sandbox.stub(NewTabUtils.activityStreamLinks, "addBookmark");
    278 
    279  let data = { url: "pear.com", title: "A pear" };
    280  let _target = { browser: { ownerGlobal() {} } };
    281  feed.onAction({ type: actionTypes.BOOKMARK_URL, data, _target });
    282  Assert.ok(
    283    NewTabUtils.activityStreamLinks.addBookmark.calledWith(
    284      data,
    285      _target.browser.ownerGlobal
    286    )
    287  );
    288 
    289  sandbox.restore();
    290 });
    291 
    292 add_task(async function test_onAction_DELETE_BOOKMARK_BY_ID() {
    293  info("PlacesFeed.onAction should delete a bookmark on DELETE_BOOKMARK_BY_ID");
    294  let sandbox = sinon.createSandbox();
    295  let feed = getPlacesFeedForTest(sandbox);
    296  sandbox.stub(NewTabUtils.activityStreamLinks, "deleteBookmark");
    297 
    298  feed.onAction({ type: actionTypes.DELETE_BOOKMARK_BY_ID, data: "g123kd" });
    299  Assert.ok(
    300    NewTabUtils.activityStreamLinks.deleteBookmark.calledWith("g123kd")
    301  );
    302 
    303  sandbox.restore();
    304 });
    305 
    306 add_task(async function test_onAction_DELETE_HISTORY_URL() {
    307  info(
    308    "PlacesFeed.onAction should delete a history entry on DELETE_HISTORY_URL"
    309  );
    310  let sandbox = sinon.createSandbox();
    311  let feed = getPlacesFeedForTest(sandbox);
    312  sandbox.stub(NewTabUtils.activityStreamLinks, "deleteHistoryEntry");
    313  sandbox.stub(NewTabUtils.activityStreamLinks, "blockURL");
    314 
    315  feed.onAction({
    316    type: actionTypes.DELETE_HISTORY_URL,
    317    data: { url: "guava.com", forceBlock: null },
    318  });
    319  Assert.ok(
    320    NewTabUtils.activityStreamLinks.deleteHistoryEntry.calledWith("guava.com")
    321  );
    322  Assert.ok(NewTabUtils.activityStreamLinks.blockURL.notCalled);
    323 
    324  sandbox.restore();
    325 });
    326 
    327 add_task(async function test_onAction_DELETE_HISTORY_URL_and_block() {
    328  info(
    329    "PlacesFeed.onAction should delete a history entry on " +
    330      "DELETE_HISTORY_URL and force a site to be blocked if specified"
    331  );
    332  let sandbox = sinon.createSandbox();
    333  let feed = getPlacesFeedForTest(sandbox);
    334  sandbox.stub(NewTabUtils.activityStreamLinks, "deleteHistoryEntry");
    335  sandbox.stub(NewTabUtils.activityStreamLinks, "blockURL");
    336 
    337  feed.onAction({
    338    type: actionTypes.DELETE_HISTORY_URL,
    339    data: { url: "guava.com", forceBlock: "g123kd" },
    340  });
    341  Assert.ok(
    342    NewTabUtils.activityStreamLinks.deleteHistoryEntry.calledWith("guava.com")
    343  );
    344  Assert.ok(
    345    NewTabUtils.activityStreamLinks.blockURL.calledWith({
    346      url: "guava.com",
    347      pocket_id: undefined,
    348    })
    349  );
    350 
    351  sandbox.restore();
    352 });
    353 
    354 add_task(async function test_onAction_OPEN_NEW_WINDOW() {
    355  info(
    356    "PlacesFeed.onAction should call openTrustedLinkIn with the " +
    357      "correct url, where and params on OPEN_NEW_WINDOW"
    358  );
    359  let sandbox = sinon.createSandbox();
    360  let feed = getPlacesFeedForTest(sandbox);
    361  let openTrustedLinkIn = sandbox.stub();
    362  let openWindowAction = {
    363    type: actionTypes.OPEN_NEW_WINDOW,
    364    data: { url: "https://foo.com" },
    365    _target: { browser: { ownerGlobal: { openTrustedLinkIn } } },
    366  };
    367 
    368  feed.onAction(openWindowAction);
    369 
    370  Assert.ok(openTrustedLinkIn.calledOnce, "openTrustedLinkIn called");
    371  let [url, where, params] = openTrustedLinkIn.firstCall.args;
    372  Assert.equal(url, "https://foo.com");
    373  Assert.equal(where, "window");
    374  Assert.ok(!params.private);
    375  Assert.ok(!params.forceForeground);
    376 
    377  sandbox.restore();
    378 });
    379 
    380 add_task(async function test_onAction_OPEN_PRIVATE_WINDOW() {
    381  info(
    382    "PlacesFeed.onAction should call openTrustedLinkIn with the " +
    383      "correct url, where, params and privacy args on OPEN_PRIVATE_WINDOW"
    384  );
    385  let sandbox = sinon.createSandbox();
    386  let feed = getPlacesFeedForTest(sandbox);
    387  let openTrustedLinkIn = sandbox.stub();
    388  let openWindowAction = {
    389    type: actionTypes.OPEN_PRIVATE_WINDOW,
    390    data: { url: "https://foo.com" },
    391    _target: { browser: { ownerGlobal: { openTrustedLinkIn } } },
    392  };
    393 
    394  feed.onAction(openWindowAction);
    395 
    396  Assert.ok(openTrustedLinkIn.calledOnce, "openTrustedLinkIn called");
    397  let [url, where, params] = openTrustedLinkIn.firstCall.args;
    398  Assert.equal(url, "https://foo.com");
    399  Assert.equal(where, "window");
    400  Assert.ok(params.private);
    401  Assert.ok(!params.forceForeground);
    402 
    403  sandbox.restore();
    404 });
    405 
    406 add_task(async function test_onAction_OPEN_LINK() {
    407  info(
    408    "PlacesFeed.onAction should call openTrustedLinkIn with the " +
    409      "correct url, where and params on OPEN_LINK"
    410  );
    411  let sandbox = sinon.createSandbox();
    412  let feed = getPlacesFeedForTest(sandbox);
    413  let openTrustedLinkIn = sandbox.stub();
    414  let openLinkAction = {
    415    type: actionTypes.OPEN_LINK,
    416    data: { url: "https://foo.com" },
    417    _target: {
    418      browser: {
    419        ownerGlobal: { openTrustedLinkIn },
    420      },
    421    },
    422  };
    423  feed.onAction(openLinkAction);
    424 
    425  Assert.ok(openTrustedLinkIn.calledOnce, "openTrustedLinkIn called");
    426  let [url, where, params] = openTrustedLinkIn.firstCall.args;
    427  Assert.equal(url, "https://foo.com");
    428  Assert.equal(where, "current");
    429  Assert.ok(!params.private);
    430  Assert.ok(!params.forceForeground);
    431 
    432  sandbox.restore();
    433 });
    434 
    435 add_task(async function test_onAction_OPEN_LINK_referrer() {
    436  info("PlacesFeed.onAction should open link with referrer on OPEN_LINK");
    437  let sandbox = sinon.createSandbox();
    438  let feed = getPlacesFeedForTest(sandbox);
    439  let openTrustedLinkIn = sandbox.stub();
    440  let openLinkAction = {
    441    type: actionTypes.OPEN_LINK,
    442    data: { url: "https://foo.com", referrer: "https://foo.com/ref" },
    443    _target: {
    444      browser: {
    445        ownerGlobal: { openTrustedLinkIn, whereToOpenLink: () => "tab" },
    446      },
    447    },
    448  };
    449  feed.onAction(openLinkAction);
    450 
    451  Assert.ok(openTrustedLinkIn.calledOnce, "openTrustedLinkIn called");
    452  let [, , params] = openTrustedLinkIn.firstCall.args;
    453  Assert.equal(params.referrerInfo.referrerPolicy, 5);
    454  Assert.equal(
    455    params.referrerInfo.originalReferrer.spec,
    456    "https://foo.com/ref"
    457  );
    458 
    459  sandbox.restore();
    460 });
    461 
    462 add_task(async function test_onAction_OPEN_LINK_typed_bonus() {
    463  info(
    464    "PlacesFeed.onAction should mark link with typed bonus as " +
    465      "typed before opening OPEN_LINK"
    466  );
    467  let sandbox = sinon.createSandbox();
    468  let callOrder = [];
    469  // We can't stub out PlacesUtils.history.markPageAsTyped, since that's an
    470  // XPCOM component. We'll stub out history instead.
    471  sandbox.stub(PlacesUtils, "history").get(() => {
    472    return {
    473      markPageAsTyped: sandbox.stub().callsFake(() => {
    474        callOrder.push("markPageAsTyped");
    475      }),
    476    };
    477  });
    478 
    479  let feed = getPlacesFeedForTest(sandbox);
    480  let openTrustedLinkIn = sandbox.stub().callsFake(() => {
    481    callOrder.push("openTrustedLinkIn");
    482  });
    483  let openLinkAction = {
    484    type: actionTypes.OPEN_LINK,
    485    data: {
    486      typedBonus: true,
    487      url: "https://foo.com",
    488    },
    489    _target: {
    490      browser: {
    491        ownerGlobal: { openTrustedLinkIn, whereToOpenLink: () => "tab" },
    492      },
    493    },
    494  };
    495  feed.onAction(openLinkAction);
    496 
    497  Assert.deepEqual(callOrder, ["markPageAsTyped", "openTrustedLinkIn"]);
    498 
    499  sandbox.restore();
    500 });
    501 
    502 add_task(async function test_onAction_OPEN_LINK_pocket() {
    503  info(
    504    "PlacesFeed.onAction should open the pocket link if it's a " +
    505      "pocket story on OPEN_LINK"
    506  );
    507  let sandbox = sinon.createSandbox();
    508  let feed = getPlacesFeedForTest(sandbox);
    509  let openTrustedLinkIn = sandbox.stub();
    510  let openLinkAction = {
    511    type: actionTypes.OPEN_LINK,
    512    data: {
    513      url: "https://foo.com",
    514      open_url: "https://getpocket.com/foo",
    515      type: "pocket",
    516    },
    517    _target: {
    518      browser: {
    519        ownerGlobal: { openTrustedLinkIn },
    520      },
    521    },
    522  };
    523 
    524  feed.onAction(openLinkAction);
    525 
    526  Assert.ok(openTrustedLinkIn.calledOnce, "openTrustedLinkIn called");
    527  let [url, where, params] = openTrustedLinkIn.firstCall.args;
    528  Assert.equal(url, "https://getpocket.com/foo");
    529  Assert.equal(where, "current");
    530  Assert.ok(!params.private);
    531  Assert.ok(!params.forceForeground);
    532 
    533  sandbox.restore();
    534 });
    535 
    536 add_task(async function test_onAction_OPEN_LINK_not_http() {
    537  info("PlacesFeed.onAction should not open link if not http");
    538  let sandbox = sinon.createSandbox();
    539  let feed = getPlacesFeedForTest(sandbox);
    540  let openTrustedLinkIn = sandbox.stub();
    541  let openLinkAction = {
    542    type: actionTypes.OPEN_LINK,
    543    data: { url: "file:///foo.com" },
    544    _target: {
    545      browser: {
    546        ownerGlobal: { openTrustedLinkIn },
    547      },
    548    },
    549  };
    550 
    551  feed.onAction(openLinkAction);
    552 
    553  Assert.ok(openTrustedLinkIn.notCalled, "openTrustedLinkIn not called");
    554 
    555  sandbox.restore();
    556 });
    557 
    558 add_task(async function test_onAction_FILL_SEARCH_TERM() {
    559  info(
    560    "PlacesFeed.onAction should call fillSearchTopSiteTerm " +
    561      "on FILL_SEARCH_TERM"
    562  );
    563  let sandbox = sinon.createSandbox();
    564  let feed = getPlacesFeedForTest(sandbox);
    565  sandbox.stub(feed, "fillSearchTopSiteTerm");
    566 
    567  feed.onAction({ type: actionTypes.FILL_SEARCH_TERM });
    568 
    569  Assert.ok(
    570    feed.fillSearchTopSiteTerm.calledOnce,
    571    "PlacesFeed.fillSearchTopSiteTerm called"
    572  );
    573  sandbox.restore();
    574 });
    575 
    576 add_task(async function test_onAction_ABOUT_SPONSORED_TOP_SITES() {
    577  info(
    578    "PlacesFeed.onAction should call openTrustedLinkIn with the " +
    579      "correct SUMO url on ABOUT_SPONSORED_TOP_SITES"
    580  );
    581  let sandbox = sinon.createSandbox();
    582  let feed = getPlacesFeedForTest(sandbox);
    583  let openTrustedLinkIn = sandbox.stub();
    584  let openLinkAction = {
    585    type: actionTypes.ABOUT_SPONSORED_TOP_SITES,
    586    _target: {
    587      browser: {
    588        ownerGlobal: { openTrustedLinkIn },
    589      },
    590    },
    591  };
    592 
    593  feed.onAction(openLinkAction);
    594 
    595  Assert.ok(openTrustedLinkIn.calledOnce, "openTrustedLinkIn called");
    596  let [url, where] = openTrustedLinkIn.firstCall.args;
    597  Assert.ok(url.endsWith("sponsor-privacy"));
    598  Assert.equal(where, "tab");
    599 
    600  sandbox.restore();
    601 });
    602 
    603 add_task(async function test_onAction_FILL_SEARCH_TERM() {
    604  info(
    605    "PlacesFeed.onAction should set the URL bar value to the label value " +
    606      "on FILL_SEARCH_TERM"
    607  );
    608  let sandbox = sinon.createSandbox();
    609  sandbox.stub(SearchService.prototype, "getEngineByAlias").resolves(null);
    610 
    611  let feed = getPlacesFeedForTest(sandbox);
    612  let locationBar = { search: sandbox.stub() };
    613  let action = {
    614    type: actionTypes.FILL_SEARCH_TERM,
    615    data: { label: "@Foo" },
    616    _target: { browser: { ownerGlobal: { gURLBar: locationBar } } },
    617  };
    618 
    619  await feed.onAction(action);
    620 
    621  Assert.ok(locationBar.search.calledOnce, "gURLBar.search called");
    622  Assert.ok(
    623    locationBar.search.calledWithExactly("@Foo", {
    624      searchEngine: null,
    625      searchModeEntry: "topsites_newtab",
    626    })
    627  );
    628 
    629  sandbox.restore();
    630 });
    631 
    632 add_task(async function test_onAction_HANDOFF_SEARCH_TO_AWESOMEBAR() {
    633  info(
    634    "PlacesFeed.onAction should call handoffSearchToAwesomebar " +
    635      "on HANDOFF_SEARCH_TO_AWESOMEBAR"
    636  );
    637  let sandbox = sinon.createSandbox();
    638 
    639  let feed = getPlacesFeedForTest(sandbox);
    640  sandbox.stub(feed, "handoffSearchToAwesomebar");
    641 
    642  let action = {
    643    type: actionTypes.HANDOFF_SEARCH_TO_AWESOMEBAR,
    644    data: { text: "f" },
    645    meta: { fromTarget: {} },
    646    _target: { browser: { ownerGlobal: { gURLBar: { focus: () => {} } } } },
    647  };
    648 
    649  await feed.onAction(action);
    650 
    651  Assert.ok(
    652    feed.handoffSearchToAwesomebar.calledOnce,
    653    "PlacesFeed.handoffSearchToAwesomebar called"
    654  );
    655  Assert.ok(feed.handoffSearchToAwesomebar.calledWithExactly(action));
    656 
    657  sandbox.restore();
    658 });
    659 
    660 add_task(async function test_onAction_PARTNER_LINK_ATTRIBUTION() {
    661  info(
    662    "PlacesFeed.onAction should call makeAttributionRequest on " +
    663      "PARTNER_LINK_ATTRIBUTION"
    664  );
    665  let sandbox = sinon.createSandbox();
    666 
    667  let feed = getPlacesFeedForTest(sandbox);
    668  sandbox.stub(feed, "makeAttributionRequest");
    669 
    670  let data = { targetURL: "https://partnersite.com", source: "topsites" };
    671  feed.onAction({
    672    type: actionTypes.PARTNER_LINK_ATTRIBUTION,
    673    data,
    674  });
    675 
    676  Assert.ok(
    677    feed.makeAttributionRequest.calledOnce,
    678    "PlacesFeed.makeAttributionRequest called"
    679  );
    680  Assert.ok(feed.makeAttributionRequest.calledWithExactly(data));
    681 
    682  sandbox.restore();
    683 });
    684 
    685 add_task(
    686  async function test_makeAttributionRequest_PartnerLinkAttribution_makeReq() {
    687    info(
    688      "PlacesFeed.makeAttributionRequest should call " +
    689        "PartnerLinkAttribution.makeRequest"
    690    );
    691    let sandbox = sinon.createSandbox();
    692 
    693    let feed = getPlacesFeedForTest(sandbox);
    694    sandbox.stub(PartnerLinkAttribution, "makeRequest");
    695 
    696    let data = { targetURL: "https://partnersite.com", source: "topsites" };
    697    feed.makeAttributionRequest(data);
    698 
    699    Assert.ok(
    700      PartnerLinkAttribution.makeRequest.calledOnce,
    701      "PartnerLinkAttribution.makeRequest called"
    702    );
    703 
    704    sandbox.restore();
    705  }
    706 );
    707 
    708 function createFakeURLBar(sandbox) {
    709  let fakeURLBar = {
    710    focus: sandbox.spy(),
    711    handoff: sandbox.spy(),
    712    setHiddenFocus: sandbox.spy(),
    713    removeHiddenFocus: sandbox.spy(),
    714    addEventListener: (ev, cb) => {
    715      fakeURLBar.listeners[ev] = cb;
    716    },
    717    removeEventListener: sandbox.spy(),
    718 
    719    listeners: [],
    720  };
    721 
    722  return fakeURLBar;
    723 }
    724 
    725 add_task(async function test_handoffSearchToAwesomebar_no_text() {
    726  info(
    727    "PlacesFeed.handoffSearchToAwesomebar should properly handle handoff " +
    728      "with no text passed in"
    729  );
    730 
    731  let sandbox = sinon.createSandbox();
    732 
    733  let feed = getPlacesFeedForTest(sandbox);
    734  let fakeURLBar = createFakeURLBar(sandbox);
    735 
    736  sandbox.stub(PrivateBrowsingUtils, "isBrowserPrivate").returns(false);
    737  sandbox.stub(feed, "_getDefaultSearchEngine").returns(null);
    738 
    739  feed.handoffSearchToAwesomebar({
    740    _target: { browser: { ownerGlobal: { gURLBar: fakeURLBar } } },
    741    data: {},
    742    meta: { fromTarget: {} },
    743  });
    744 
    745  Assert.ok(
    746    fakeURLBar.setHiddenFocus.calledOnce,
    747    "gURLBar.setHiddenFocus called"
    748  );
    749  Assert.ok(fakeURLBar.handoff.notCalled, "gURLBar.handoff not called");
    750  Assert.ok(
    751    feed.store.dispatch.notCalled,
    752    "PlacesFeed.store.dispatch not called"
    753  );
    754 
    755  // Now type a character.
    756  fakeURLBar.listeners.keydown({ key: "f" });
    757  Assert.ok(fakeURLBar.handoff.calledOnce, "gURLBar.handoff called");
    758  Assert.ok(
    759    fakeURLBar.removeHiddenFocus.calledOnce,
    760    "gURLBar.removeHiddenFocus called"
    761  );
    762  Assert.ok(feed.store.dispatch.calledOnce, "PlacesFeed.store.dispatch called");
    763  Assert.ok(
    764    feed.store.dispatch.calledWith({
    765      meta: {
    766        from: "ActivityStream:Main",
    767        skipMain: true,
    768        to: "ActivityStream:Content",
    769        toTarget: {},
    770      },
    771      type: "DISABLE_SEARCH",
    772    }),
    773    "PlacesFeed.store.dispatch called"
    774  );
    775 
    776  sandbox.restore();
    777 });
    778 
    779 add_task(async function test_handoffSearchToAwesomebar_with_text() {
    780  info(
    781    "PlacesFeed.handoffSearchToAwesomebar should properly handle handoff " +
    782      "with text data passed in"
    783  );
    784 
    785  let sandbox = sinon.createSandbox();
    786 
    787  let feed = getPlacesFeedForTest(sandbox);
    788  let fakeURLBar = createFakeURLBar(sandbox);
    789 
    790  sandbox.stub(PrivateBrowsingUtils, "isBrowserPrivate").returns(false);
    791  let engine = {};
    792  sandbox.stub(feed, "_getDefaultSearchEngine").returns(engine);
    793 
    794  const SESSION_ID = "decafc0ffee";
    795  AboutNewTab.activityStream.store.feeds.get.returns({
    796    sessions: {
    797      get: () => {
    798        return { session_id: SESSION_ID };
    799      },
    800    },
    801  });
    802 
    803  feed.handoffSearchToAwesomebar({
    804    _target: { browser: { ownerGlobal: { gURLBar: fakeURLBar } } },
    805    data: { text: "foo" },
    806    meta: { fromTarget: {} },
    807  });
    808 
    809  Assert.ok(fakeURLBar.handoff.calledOnce, "gURLBar.handoff was called");
    810  Assert.ok(fakeURLBar.handoff.calledWithExactly("foo", engine, SESSION_ID));
    811  Assert.ok(fakeURLBar.focus.notCalled, "gURLBar.focus not called");
    812  Assert.ok(
    813    fakeURLBar.setHiddenFocus.notCalled,
    814    "gURLBar.setHiddenFocus not called"
    815  );
    816 
    817  // Now call blur listener.
    818  fakeURLBar.listeners.blur();
    819  Assert.ok(feed.store.dispatch.calledOnce, "PlacesFeed.store.dispatch called");
    820  Assert.ok(
    821    feed.store.dispatch.calledWith({
    822      meta: {
    823        from: "ActivityStream:Main",
    824        skipMain: true,
    825        to: "ActivityStream:Content",
    826        toTarget: {},
    827      },
    828      type: "SHOW_SEARCH",
    829    })
    830  );
    831 
    832  sandbox.restore();
    833 });
    834 
    835 add_task(async function test_handoffSearchToAwesomebar_with_text_pb_mode() {
    836  info(
    837    "PlacesFeed.handoffSearchToAwesomebar should properly handle handoff " +
    838      "with text data passed in, in private browsing mode"
    839  );
    840 
    841  let sandbox = sinon.createSandbox();
    842 
    843  let feed = getPlacesFeedForTest(sandbox);
    844  let fakeURLBar = createFakeURLBar(sandbox);
    845 
    846  sandbox.stub(PrivateBrowsingUtils, "isBrowserPrivate").returns(true);
    847  let engine = {};
    848  sandbox.stub(feed, "_getDefaultSearchEngine").returns(engine);
    849 
    850  feed.handoffSearchToAwesomebar({
    851    _target: { browser: { ownerGlobal: { gURLBar: fakeURLBar } } },
    852    data: { text: "foo" },
    853    meta: { fromTarget: {} },
    854  });
    855  Assert.ok(fakeURLBar.handoff.calledOnce, "gURLBar.handoff was called");
    856  Assert.ok(fakeURLBar.handoff.calledWithExactly("foo", engine, undefined));
    857  Assert.ok(fakeURLBar.focus.notCalled, "gURLBar.focus not called");
    858  Assert.ok(
    859    fakeURLBar.setHiddenFocus.notCalled,
    860    "gURLBar.setHiddenFocus not called"
    861  );
    862 
    863  // Now call blur listener.
    864  fakeURLBar.listeners.blur();
    865  Assert.ok(feed.store.dispatch.calledOnce, "PlacesFeed.store.dispatch called");
    866  Assert.ok(
    867    feed.store.dispatch.calledWith({
    868      meta: {
    869        from: "ActivityStream:Main",
    870        skipMain: true,
    871        to: "ActivityStream:Content",
    872        toTarget: {},
    873      },
    874      type: "SHOW_SEARCH",
    875    })
    876  );
    877 
    878  sandbox.restore();
    879 });
    880 
    881 add_task(async function test_handoffSearchToAwesomebar_SHOW_SEARCH_on_esc() {
    882  info(
    883    "PlacesFeed.handoffSearchToAwesomebar should SHOW_SEARCH on ESC keydown"
    884  );
    885 
    886  let sandbox = sinon.createSandbox();
    887 
    888  let feed = getPlacesFeedForTest(sandbox);
    889  let fakeURLBar = createFakeURLBar(sandbox);
    890 
    891  sandbox.stub(PrivateBrowsingUtils, "isBrowserPrivate").returns(false);
    892  let engine = {};
    893  sandbox.stub(feed, "_getDefaultSearchEngine").returns(engine);
    894 
    895  feed.handoffSearchToAwesomebar({
    896    _target: { browser: { ownerGlobal: { gURLBar: fakeURLBar } } },
    897    data: { text: "foo" },
    898    meta: { fromTarget: {} },
    899  });
    900  Assert.ok(fakeURLBar.handoff.calledOnce, "gURLBar.handoff was called");
    901  Assert.ok(fakeURLBar.handoff.calledWithExactly("foo", engine, undefined));
    902  Assert.ok(fakeURLBar.focus.notCalled, "gURLBar.focus not called");
    903 
    904  // Now call ESC keydown.
    905  fakeURLBar.listeners.keydown({ key: "Escape" });
    906  Assert.ok(feed.store.dispatch.calledOnce, "PlacesFeed.store.dispatch called");
    907  Assert.ok(
    908    feed.store.dispatch.calledWith({
    909      meta: {
    910        from: "ActivityStream:Main",
    911        skipMain: true,
    912        to: "ActivityStream:Content",
    913        toTarget: {},
    914      },
    915      type: "SHOW_SEARCH",
    916    })
    917  );
    918 
    919  sandbox.restore();
    920 });
    921 
    922 add_task(
    923  async function test_handoffSearchToAwesomebar_with_session_id_no_text() {
    924    info(
    925      "PlacesFeed.handoffSearchToAwesomebar should properly handoff a " +
    926        "newtab session id with no text passed in"
    927    );
    928 
    929    let sandbox = sinon.createSandbox();
    930 
    931    let feed = getPlacesFeedForTest(sandbox);
    932    let fakeURLBar = createFakeURLBar(sandbox);
    933 
    934    sandbox.stub(PrivateBrowsingUtils, "isBrowserPrivate").returns(false);
    935    let engine = {};
    936    sandbox.stub(feed, "_getDefaultSearchEngine").returns(engine);
    937 
    938    const SESSION_ID = "decafc0ffee";
    939    AboutNewTab.activityStream.store.feeds.get.returns({
    940      sessions: {
    941        get: () => {
    942          return { session_id: SESSION_ID };
    943        },
    944      },
    945    });
    946 
    947    feed.handoffSearchToAwesomebar({
    948      _target: { browser: { ownerGlobal: { gURLBar: fakeURLBar } } },
    949      data: {},
    950      meta: { fromTarget: {} },
    951    });
    952 
    953    Assert.ok(
    954      fakeURLBar.setHiddenFocus.calledOnce,
    955      "gURLBar.setHiddenFocus was called"
    956    );
    957    Assert.ok(fakeURLBar.handoff.notCalled, "gURLBar.handoff not called");
    958    Assert.ok(fakeURLBar.focus.notCalled, "gURLBar.focus not called");
    959    Assert.ok(
    960      feed.store.dispatch.notCalled,
    961      "PlacesFeed.store.dispatch not called"
    962    );
    963 
    964    // Now type a character.
    965    fakeURLBar.listeners.keydown({ key: "f" });
    966    Assert.ok(fakeURLBar.handoff.calledOnce, "gURLBar.handoff was called");
    967    Assert.ok(fakeURLBar.handoff.calledWithExactly("", engine, SESSION_ID));
    968 
    969    Assert.ok(
    970      fakeURLBar.removeHiddenFocus.calledOnce,
    971      "gURLBar.removeHiddenFocus was called"
    972    );
    973    Assert.ok(
    974      feed.store.dispatch.calledOnce,
    975      "PlacesFeed.store.dispatch called"
    976    );
    977    Assert.ok(
    978      feed.store.dispatch.calledWith({
    979        meta: {
    980          from: "ActivityStream:Main",
    981          skipMain: true,
    982          to: "ActivityStream:Content",
    983          toTarget: {},
    984        },
    985        type: "DISABLE_SEARCH",
    986      })
    987    );
    988 
    989    sandbox.restore();
    990  }
    991 );
    992 
    993 add_task(async function test_observe_dispatch_PLACES_LINK_BLOCKED() {
    994  info(
    995    "PlacesFeed.observe should dispatch a PLACES_LINK_BLOCKED action " +
    996      "with the url of the blocked link"
    997  );
    998 
    999  let sandbox = sinon.createSandbox();
   1000 
   1001  let feed = getPlacesFeedForTest(sandbox);
   1002  feed.observe(null, BLOCKED_EVENT, "foo123.com");
   1003  Assert.equal(
   1004    feed.store.dispatch.firstCall.args[0].type,
   1005    actionTypes.PLACES_LINK_BLOCKED
   1006  );
   1007  Assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, {
   1008    url: "foo123.com",
   1009  });
   1010 
   1011  sandbox.restore();
   1012 });
   1013 
   1014 add_task(async function test_observe_no_dispatch() {
   1015  info(
   1016    "PlacesFeed.observe should not call dispatch if the topic is something " +
   1017      "other than BLOCKED_EVENT"
   1018  );
   1019 
   1020  let sandbox = sinon.createSandbox();
   1021 
   1022  let feed = getPlacesFeedForTest(sandbox);
   1023  feed.observe(null, "someotherevent");
   1024  Assert.ok(
   1025    feed.store.dispatch.notCalled,
   1026    "PlacesFeed.store.dispatch not called"
   1027  );
   1028 
   1029  sandbox.restore();
   1030 });
   1031 
   1032 add_task(
   1033  async function test_handlePlacesEvent_dispatch_one_PLACES_LINKS_CHANGED() {
   1034    let events = [
   1035      {
   1036        message:
   1037          "PlacesFeed.handlePlacesEvent should only dispatch 1 PLACES_LINKS_CHANGED action " +
   1038          "if many bookmark-added notifications happened at once",
   1039        dispatchCallCount: 5,
   1040        event: {
   1041          itemType: TYPE_BOOKMARK,
   1042          source: SOURCES.DEFAULT,
   1043          dateAdded: FAKE_BOOKMARK.dateAdded,
   1044          guid: FAKE_BOOKMARK.bookmarkGuid,
   1045          title: FAKE_BOOKMARK.bookmarkTitle,
   1046          url: "https://www.foo.com",
   1047          isTagging: false,
   1048          type: "bookmark-added",
   1049        },
   1050      },
   1051      {
   1052        message:
   1053          "PlacesFeed.handlePlacesEvent should only dispatch 1 " +
   1054          "PLACES_LINKS_CHANGED action if many onItemRemoved notifications " +
   1055          "happened at once",
   1056        dispatchCallCount: 5,
   1057        event: {
   1058          id: null,
   1059          parentId: null,
   1060          index: null,
   1061          itemType: TYPE_BOOKMARK,
   1062          url: "foo.com",
   1063          guid: "rTU_oiklsU7D",
   1064          parentGuid: "2BzBQXOPFmuU",
   1065          source: SOURCES.DEFAULT,
   1066          type: "bookmark-removed",
   1067        },
   1068      },
   1069      {
   1070        message:
   1071          "PlacesFeed.handlePlacesEvent should only dispatch 1 " +
   1072          "PLACES_LINKS_CHANGED action if any page-removed notifications " +
   1073          "happened at once",
   1074        dispatchCallCount: 5,
   1075        event: {
   1076          type: "page-removed",
   1077          url: "foo.com",
   1078          isRemovedFromStore: true,
   1079        },
   1080      },
   1081    ];
   1082 
   1083    for (let { message, dispatchCallCount, event } of events) {
   1084      info(message);
   1085 
   1086      let sandbox = sinon.createSandbox();
   1087      let feed = getPlacesFeedForTest(sandbox);
   1088 
   1089      await feed.placesObserver.handlePlacesEvent([event]);
   1090      await feed.placesObserver.handlePlacesEvent([event]);
   1091      await feed.placesObserver.handlePlacesEvent([event]);
   1092      await feed.placesObserver.handlePlacesEvent([event]);
   1093 
   1094      Assert.ok(feed.placesChangedTimer, "PlacesFeed dispatch timer created");
   1095 
   1096      // Let's speed things up a bit.
   1097      feed.placesChangedTimer.delay = 0;
   1098 
   1099      // Wait for the timer to go off and get cleared
   1100      await TestUtils.waitForCondition(
   1101        () => !feed.placesChangedTimer,
   1102        "PlacesFeed dispatch timer cleared"
   1103      );
   1104 
   1105      Assert.equal(
   1106        feed.store.dispatch.callCount,
   1107        dispatchCallCount,
   1108        `PlacesFeed.store.dispatch was called ${dispatchCallCount} times`
   1109      );
   1110 
   1111      Assert.ok(
   1112        feed.store.dispatch.withArgs(
   1113          actionCreators.OnlyToMain({ type: actionTypes.PLACES_LINKS_CHANGED })
   1114        ).calledOnce,
   1115        "PlacesFeed.store.dispatch called with PLACES_LINKS_CHANGED once"
   1116      );
   1117 
   1118      sandbox.restore();
   1119    }
   1120  }
   1121 );
   1122 
   1123 add_task(async function test_PlacesObserver_dispatches() {
   1124  let events = [
   1125    {
   1126      message:
   1127        "PlacesObserver should dispatch a PLACES_HISTORY_CLEARED action " +
   1128        "on history-cleared",
   1129      args: { type: "history-cleared" },
   1130      expectedAction: { type: actionTypes.PLACES_HISTORY_CLEARED },
   1131    },
   1132    {
   1133      message:
   1134        "PlacesObserver should dispatch a PLACES_LINKS_DELETED action " +
   1135        "with the right url",
   1136      args: {
   1137        type: "page-removed",
   1138        url: "foo.com",
   1139        isRemovedFromStore: true,
   1140      },
   1141      expectedAction: {
   1142        type: actionTypes.PLACES_LINKS_DELETED,
   1143        data: { urls: ["foo.com"] },
   1144      },
   1145    },
   1146    {
   1147      message:
   1148        "PlacesObserver should dispatch a PLACES_BOOKMARK_ADDED action with " +
   1149        "the bookmark data - http",
   1150      args: {
   1151        itemType: TYPE_BOOKMARK,
   1152        source: SOURCES.DEFAULT,
   1153        dateAdded: FAKE_BOOKMARK.dateAdded,
   1154        guid: FAKE_BOOKMARK.bookmarkGuid,
   1155        title: FAKE_BOOKMARK.bookmarkTitle,
   1156        url: "http://www.foo.com",
   1157        isTagging: false,
   1158        type: "bookmark-added",
   1159      },
   1160      expectedAction: {
   1161        type: actionTypes.PLACES_BOOKMARK_ADDED,
   1162        data: {
   1163          bookmarkGuid: FAKE_BOOKMARK.bookmarkGuid,
   1164          bookmarkTitle: FAKE_BOOKMARK.bookmarkTitle,
   1165          dateAdded: FAKE_BOOKMARK.dateAdded * 1000,
   1166          url: "http://www.foo.com",
   1167        },
   1168      },
   1169    },
   1170    {
   1171      message:
   1172        "PlacesObserver should dispatch a PLACES_BOOKMARK_ADDED action with " +
   1173        "the bookmark data - https",
   1174      args: {
   1175        itemType: TYPE_BOOKMARK,
   1176        source: SOURCES.DEFAULT,
   1177        dateAdded: FAKE_BOOKMARK.dateAdded,
   1178        guid: FAKE_BOOKMARK.bookmarkGuid,
   1179        title: FAKE_BOOKMARK.bookmarkTitle,
   1180        url: "https://www.foo.com",
   1181        isTagging: false,
   1182        type: "bookmark-added",
   1183      },
   1184      expectedAction: {
   1185        type: actionTypes.PLACES_BOOKMARK_ADDED,
   1186        data: {
   1187          bookmarkGuid: FAKE_BOOKMARK.bookmarkGuid,
   1188          bookmarkTitle: FAKE_BOOKMARK.bookmarkTitle,
   1189          dateAdded: FAKE_BOOKMARK.dateAdded * 1000,
   1190          url: "https://www.foo.com",
   1191        },
   1192      },
   1193    },
   1194  ];
   1195 
   1196  for (let { message, args, expectedAction } of events) {
   1197    info(message);
   1198    let sandbox = sinon.createSandbox();
   1199    let dispatch = sandbox.spy();
   1200    let observer = new PlacesFeed.PlacesObserver(dispatch);
   1201    await observer.handlePlacesEvent([args]);
   1202    Assert.ok(dispatch.calledWith(expectedAction));
   1203    sandbox.restore();
   1204  }
   1205 });
   1206 
   1207 add_task(async function test_PlacesObserver_ignores() {
   1208  let events = [
   1209    {
   1210      message:
   1211        "PlacesObserver should not dispatch a PLACES_BOOKMARK_ADDED action - " +
   1212        "not http/https for bookmark-added",
   1213      event: {
   1214        itemType: TYPE_BOOKMARK,
   1215        source: SOURCES.DEFAULT,
   1216        dateAdded: FAKE_BOOKMARK.dateAdded,
   1217        guid: FAKE_BOOKMARK.bookmarkGuid,
   1218        title: FAKE_BOOKMARK.bookmarkTitle,
   1219        url: "foo.com",
   1220        isTagging: false,
   1221        type: "bookmark-added",
   1222      },
   1223    },
   1224    {
   1225      message:
   1226        "PlacesObserver should not dispatch a PLACES_BOOKMARK_ADDED action - " +
   1227        "has IMPORT source for bookmark-added",
   1228      event: {
   1229        itemType: TYPE_BOOKMARK,
   1230        source: SOURCES.IMPORT,
   1231        dateAdded: FAKE_BOOKMARK.dateAdded,
   1232        guid: FAKE_BOOKMARK.bookmarkGuid,
   1233        title: FAKE_BOOKMARK.bookmarkTitle,
   1234        url: "foo.com",
   1235        isTagging: false,
   1236        type: "bookmark-added",
   1237      },
   1238    },
   1239    {
   1240      message:
   1241        "PlacesObserver should not dispatch a PLACES_BOOKMARK_ADDED " +
   1242        "action - has RESTORE source for bookmark-added",
   1243      event: {
   1244        itemType: TYPE_BOOKMARK,
   1245        source: SOURCES.RESTORE,
   1246        dateAdded: FAKE_BOOKMARK.dateAdded,
   1247        guid: FAKE_BOOKMARK.bookmarkGuid,
   1248        title: FAKE_BOOKMARK.bookmarkTitle,
   1249        url: "foo.com",
   1250        isTagging: false,
   1251        type: "bookmark-added",
   1252      },
   1253    },
   1254    {
   1255      message:
   1256        "PlacesObserver should not dispatch a PLACES_BOOKMARK_ADDED " +
   1257        "action - has RESTORE_ON_STARTUP source for bookmark-added",
   1258      event: {
   1259        itemType: TYPE_BOOKMARK,
   1260        source: SOURCES.RESTORE_ON_STARTUP,
   1261        dateAdded: FAKE_BOOKMARK.dateAdded,
   1262        guid: FAKE_BOOKMARK.bookmarkGuid,
   1263        title: FAKE_BOOKMARK.bookmarkTitle,
   1264        url: "foo.com",
   1265        isTagging: false,
   1266        type: "bookmark-added",
   1267      },
   1268    },
   1269    {
   1270      message:
   1271        "PlacesObserver should not dispatch a PLACES_BOOKMARK_ADDED " +
   1272        "action - has SYNC source for bookmark-added",
   1273      event: {
   1274        itemType: TYPE_BOOKMARK,
   1275        source: SOURCES.SYNC,
   1276        dateAdded: FAKE_BOOKMARK.dateAdded,
   1277        guid: FAKE_BOOKMARK.bookmarkGuid,
   1278        title: FAKE_BOOKMARK.bookmarkTitle,
   1279        url: "foo.com",
   1280        isTagging: false,
   1281        type: "bookmark-added",
   1282      },
   1283    },
   1284    {
   1285      message:
   1286        "PlacesObserver should ignore events that are not of " +
   1287        "TYPE_BOOKMARK for bookmark-added",
   1288      event: {
   1289        itemType: "nottypebookmark",
   1290        source: SOURCES.DEFAULT,
   1291        dateAdded: FAKE_BOOKMARK.dateAdded,
   1292        guid: FAKE_BOOKMARK.bookmarkGuid,
   1293        title: FAKE_BOOKMARK.bookmarkTitle,
   1294        url: "https://www.foo.com",
   1295        isTagging: false,
   1296        type: "bookmark-added",
   1297      },
   1298    },
   1299    {
   1300      message:
   1301        "PlacesObserver should ignore events that are not of " +
   1302        "TYPE_BOOKMARK for bookmark-removed",
   1303      event: {
   1304        id: null,
   1305        parentId: null,
   1306        index: null,
   1307        itemType: "nottypebookmark",
   1308        url: null,
   1309        guid: "461Z_7daEqIh",
   1310        parentGuid: "hkHScG3aI3hh",
   1311        source: SOURCES.DEFAULT,
   1312        type: "bookmark-removed",
   1313      },
   1314    },
   1315    {
   1316      message:
   1317        "PlacesObserver should not dispatch a PLACES_BOOKMARKS_REMOVED " +
   1318        "action - has SYNC source for bookmark-removed",
   1319      event: {
   1320        id: null,
   1321        parentId: null,
   1322        index: null,
   1323        itemType: TYPE_BOOKMARK,
   1324        url: "foo.com",
   1325        guid: "uvRE3stjoZOI",
   1326        parentGuid: "BnsXZl8VMJjB",
   1327        source: SOURCES.SYNC,
   1328        type: "bookmark-removed",
   1329      },
   1330    },
   1331    {
   1332      message:
   1333        "PlacesObserver should not dispatch a PLACES_BOOKMARKS_REMOVED " +
   1334        "action - has IMPORT source for bookmark-removed",
   1335      event: {
   1336        id: null,
   1337        parentId: null,
   1338        index: null,
   1339        itemType: TYPE_BOOKMARK,
   1340        url: "foo.com",
   1341        guid: "VF6YwhGpHrOW",
   1342        parentGuid: "7Vz8v9nKcSoq",
   1343        source: SOURCES.IMPORT,
   1344        type: "bookmark-removed",
   1345      },
   1346    },
   1347    {
   1348      message:
   1349        "PlacesObserver should not dispatch a PLACES_BOOKMARKS_REMOVED " +
   1350        "action - has RESTORE source for bookmark-removed",
   1351      event: {
   1352        id: null,
   1353        parentId: null,
   1354        index: null,
   1355        itemType: TYPE_BOOKMARK,
   1356        url: "foo.com",
   1357        guid: "eKozFyXJP97R",
   1358        parentGuid: "ya8Z2FbjKnD0",
   1359        source: SOURCES.RESTORE,
   1360        type: "bookmark-removed",
   1361      },
   1362    },
   1363    {
   1364      message:
   1365        "PlacesObserver should not dispatch a PLACES_BOOKMARKS_REMOVED " +
   1366        "action - has RESTORE_ON_STARTUP source for bookmark-removed",
   1367      event: {
   1368        id: null,
   1369        parentId: null,
   1370        index: null,
   1371        itemType: TYPE_BOOKMARK,
   1372        url: "foo.com",
   1373        guid: "StSGMhrYYfyD",
   1374        parentGuid: "vL8wsCe2j_eT",
   1375        source: SOURCES.RESTORE_ON_STARTUP,
   1376        type: "bookmark-removed",
   1377      },
   1378    },
   1379  ];
   1380 
   1381  for (let { message, event } of events) {
   1382    info(message);
   1383    let sandbox = sinon.createSandbox();
   1384    let dispatch = sandbox.spy();
   1385    let observer = new PlacesFeed.PlacesObserver(dispatch);
   1386 
   1387    await observer.handlePlacesEvent([event]);
   1388    Assert.ok(dispatch.notCalled, "PlacesObserver.dispatch not called");
   1389    sandbox.restore();
   1390  }
   1391 });
   1392 
   1393 add_task(async function test_PlacesObserver_bookmark_removed() {
   1394  info(
   1395    "PlacesObserver should dispatch a PLACES_BOOKMARKS_REMOVED " +
   1396      "action with the right URL and bookmarkGuid for bookmark-removed"
   1397  );
   1398  let sandbox = sinon.createSandbox();
   1399  let dispatch = sandbox.spy();
   1400  let observer = new PlacesFeed.PlacesObserver(dispatch);
   1401 
   1402  await observer.handlePlacesEvent([
   1403    {
   1404      id: null,
   1405      parentId: null,
   1406      index: null,
   1407      itemType: TYPE_BOOKMARK,
   1408      url: "foo.com",
   1409      guid: "Xgnxs27I9JnX",
   1410      parentGuid: "a4k739PL55sP",
   1411      source: SOURCES.DEFAULT,
   1412      type: "bookmark-removed",
   1413    },
   1414  ]);
   1415 
   1416  Assert.ok(
   1417    dispatch.calledWith({
   1418      type: actionTypes.PLACES_BOOKMARKS_REMOVED,
   1419      data: { urls: ["foo.com"] },
   1420    })
   1421  );
   1422  sandbox.restore();
   1423 });