TopSitesFeed.test.js 61 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437
  1. "use strict";
  2. import {actionCreators as ac, actionTypes as at} from "common/Actions.jsm";
  3. import {FakePrefs, GlobalOverrider} from "test/unit/utils";
  4. import {insertPinned, TOP_SITES_DEFAULT_ROWS, TOP_SITES_MAX_SITES_PER_ROW} from "common/Reducers.jsm";
  5. import {getDefaultOptions} from "lib/ActivityStreamStorage.jsm";
  6. import injector from "inject!lib/TopSitesFeed.jsm";
  7. import {Screenshots} from "lib/Screenshots.jsm";
  8. const FAKE_FAVICON = "data987";
  9. const FAKE_FAVICON_SIZE = 128;
  10. const FAKE_FRECENCY = 200;
  11. const FAKE_LINKS = new Array(2 * TOP_SITES_MAX_SITES_PER_ROW).fill(null).map((v, i) => ({
  12. frecency: FAKE_FRECENCY,
  13. url: `http://www.site${i}.com`,
  14. }));
  15. const FAKE_SCREENSHOT = "data123";
  16. const SEARCH_SHORTCUTS_EXPERIMENT_PREF = "improvesearch.topSiteSearchShortcuts";
  17. const SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF = "improvesearch.topSiteSearchShortcuts.searchEngines";
  18. const SEARCH_SHORTCUTS_HAVE_PINNED_PREF = "improvesearch.topSiteSearchShortcuts.havePinned";
  19. function FakeTippyTopProvider() {}
  20. FakeTippyTopProvider.prototype = {
  21. async init() { this.initialized = true; },
  22. processSite(site) { return site; },
  23. };
  24. describe("Top Sites Feed", () => {
  25. let TopSitesFeed;
  26. let DEFAULT_TOP_SITES;
  27. let feed;
  28. let globals;
  29. let sandbox;
  30. let links;
  31. let fakeNewTabUtils;
  32. let fakeScreenshot;
  33. let filterAdultStub;
  34. let shortURLStub;
  35. let fakePageThumbs;
  36. beforeEach(() => {
  37. globals = new GlobalOverrider();
  38. sandbox = globals.sandbox;
  39. fakeNewTabUtils = {
  40. blockedLinks: {
  41. links: [],
  42. isBlocked: () => false,
  43. unblock: sandbox.spy(),
  44. },
  45. activityStreamLinks: {getTopSites: sandbox.spy(() => Promise.resolve(links))},
  46. activityStreamProvider: {
  47. _addFavicons: sandbox.spy(l => Promise.resolve(l.map(link => {
  48. link.favicon = FAKE_FAVICON;
  49. link.faviconSize = FAKE_FAVICON_SIZE;
  50. return link;
  51. }))),
  52. _faviconBytesToDataURI: sandbox.spy(),
  53. },
  54. pinnedLinks: {
  55. links: [],
  56. isPinned: () => false,
  57. pin: sandbox.spy(),
  58. unpin: sandbox.spy(),
  59. },
  60. };
  61. fakeScreenshot = {
  62. getScreenshotForURL: sandbox.spy(() => Promise.resolve(FAKE_SCREENSHOT)),
  63. maybeCacheScreenshot: sandbox.spy(Screenshots.maybeCacheScreenshot),
  64. _shouldGetScreenshots: sinon.stub().returns(true),
  65. };
  66. filterAdultStub = sinon.stub().returns([]);
  67. shortURLStub = sinon.stub().callsFake(site => site.url.replace(/(.com|.ca)/, "").replace("https://", ""));
  68. const fakeDedupe = function() {};
  69. fakePageThumbs = {
  70. addExpirationFilter: sinon.stub(),
  71. removeExpirationFilter: sinon.stub(),
  72. };
  73. globals.set("PageThumbs", fakePageThumbs);
  74. globals.set("NewTabUtils", fakeNewTabUtils);
  75. sandbox.spy(global.XPCOMUtils, "defineLazyGetter");
  76. FakePrefs.prototype.prefs["default.sites"] = "https://foo.com/";
  77. ({TopSitesFeed, DEFAULT_TOP_SITES} = injector({
  78. "lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs},
  79. "common/Dedupe.jsm": {Dedupe: fakeDedupe},
  80. "common/Reducers.jsm": {insertPinned, TOP_SITES_DEFAULT_ROWS, TOP_SITES_MAX_SITES_PER_ROW},
  81. "lib/FilterAdult.jsm": {filterAdult: filterAdultStub},
  82. "lib/Screenshots.jsm": {Screenshots: fakeScreenshot},
  83. "lib/TippyTopProvider.jsm": {TippyTopProvider: FakeTippyTopProvider},
  84. "lib/ShortURL.jsm": {shortURL: shortURLStub},
  85. "lib/ActivityStreamStorage.jsm": {ActivityStreamStorage: function Fake() {}, getDefaultOptions},
  86. }));
  87. feed = new TopSitesFeed();
  88. const storage = {
  89. init: sandbox.stub().resolves(),
  90. get: sandbox.stub().resolves(),
  91. set: sandbox.stub().resolves(),
  92. };
  93. // Setup for tests that don't call `init` but require feed.storage
  94. feed._storage = storage;
  95. feed.store = {
  96. dispatch: sinon.spy(),
  97. getState() { return this.state; },
  98. state: {
  99. Prefs: {values: {filterAdult: false, topSitesRows: 2}},
  100. TopSites: {rows: Array(12).fill("site")},
  101. },
  102. dbStorage: {getDbTable: sandbox.stub().returns(storage)},
  103. };
  104. feed.dedupe.group = (...sites) => sites;
  105. links = FAKE_LINKS;
  106. // Turn off the search shortcuts experiment by default for other tests
  107. feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = false;
  108. feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = "google,amazon";
  109. });
  110. afterEach(() => {
  111. globals.restore();
  112. sandbox.restore();
  113. });
  114. function stubFaviconsToUseScreenshots() {
  115. fakeNewTabUtils.activityStreamProvider._addFavicons = sandbox.stub();
  116. }
  117. describe("#constructor", () => {
  118. it("should defineLazyGetter for _currentSearchHostname", () => {
  119. assert.calledOnce(global.XPCOMUtils.defineLazyGetter);
  120. assert.calledWith(global.XPCOMUtils.defineLazyGetter, feed, "_currentSearchHostname", sinon.match.func);
  121. });
  122. });
  123. describe("#refreshDefaults", () => {
  124. it("should add defaults on PREFS_INITIAL_VALUES", () => {
  125. feed.onAction({type: at.PREFS_INITIAL_VALUES, data: {"default.sites": "https://foo.com"}});
  126. assert.isAbove(DEFAULT_TOP_SITES.length, 0);
  127. });
  128. it("should add defaults on default.sites PREF_CHANGED", () => {
  129. feed.onAction({type: at.PREF_CHANGED, data: {name: "default.sites", value: "https://foo.com"}});
  130. assert.isAbove(DEFAULT_TOP_SITES.length, 0);
  131. });
  132. it("should refresh on topSiteRows PREF_CHANGED", () => {
  133. feed.refresh = sinon.spy();
  134. feed.onAction({type: at.PREF_CHANGED, data: {name: "topSitesRows"}});
  135. assert.calledOnce(feed.refresh);
  136. });
  137. it("should have default sites with .isDefault = true", () => {
  138. feed.refreshDefaults("https://foo.com");
  139. DEFAULT_TOP_SITES.forEach(link => assert.propertyVal(link, "isDefault", true));
  140. });
  141. it("should have default sites with appropriate hostname", () => {
  142. feed.refreshDefaults("https://foo.com");
  143. DEFAULT_TOP_SITES.forEach(link => assert.propertyVal(link, "hostname",
  144. shortURLStub(link)));
  145. });
  146. it("should add no defaults on empty pref", () => {
  147. feed.refreshDefaults("");
  148. assert.equal(DEFAULT_TOP_SITES.length, 0);
  149. });
  150. it("should clear defaults", () => {
  151. feed.refreshDefaults("https://foo.com");
  152. feed.refreshDefaults("");
  153. assert.equal(DEFAULT_TOP_SITES.length, 0);
  154. });
  155. });
  156. describe("#filterForThumbnailExpiration", () => {
  157. it("should pass rows.urls to the callback provided", () => {
  158. const rows = [{url: "foo.com"}, {"url": "bar.com", "customScreenshotURL": "custom"}];
  159. feed.store.state.TopSites = {rows};
  160. const stub = sinon.stub();
  161. feed.filterForThumbnailExpiration(stub);
  162. assert.calledOnce(stub);
  163. assert.calledWithExactly(stub, ["foo.com", "bar.com", "custom"]);
  164. });
  165. });
  166. describe("#getLinksWithDefaults", () => {
  167. beforeEach(() => {
  168. feed.refreshDefaults("https://foo.com");
  169. });
  170. describe("general", () => {
  171. it("should get the links from NewTabUtils", async () => {
  172. const result = await feed.getLinksWithDefaults();
  173. const reference = links.map(site => Object.assign({}, site, {
  174. hostname: shortURLStub(site),
  175. typedBonus: true,
  176. }));
  177. assert.deepEqual(result, reference);
  178. assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites);
  179. });
  180. it("should indicate the links get typed bonus", async () => {
  181. const result = await feed.getLinksWithDefaults();
  182. assert.propertyVal(result[0], "typedBonus", true);
  183. });
  184. it("should not filter out adult sites when pref is false", async () => {
  185. await feed.getLinksWithDefaults();
  186. assert.notCalled(filterAdultStub);
  187. });
  188. it("should filter out non-pinned adult sites when pref is true", async () => {
  189. feed.store.state.Prefs.values.filterAdult = true;
  190. fakeNewTabUtils.pinnedLinks.links = [{url: "https://foo.com/"}];
  191. const result = await feed.getLinksWithDefaults();
  192. // The stub filters out everything
  193. assert.calledOnce(filterAdultStub);
  194. assert.equal(result.length, 1);
  195. assert.equal(result[0].url, fakeNewTabUtils.pinnedLinks.links[0].url);
  196. });
  197. it("should filter out the defaults that have been blocked", async () => {
  198. // make sure we only have one top site, and we block the only default site we have to show
  199. const url = "www.myonlytopsite.com";
  200. const topsite = {
  201. frecency: FAKE_FRECENCY,
  202. hostname: shortURLStub({url}),
  203. typedBonus: true,
  204. url,
  205. };
  206. const blockedDefaultSite = {url: "https://foo.com"};
  207. fakeNewTabUtils.activityStreamLinks.getTopSites = () => [topsite];
  208. fakeNewTabUtils.blockedLinks.isBlocked = site => (site.url === blockedDefaultSite.url);
  209. const result = await feed.getLinksWithDefaults();
  210. // what we should be left with is just the top site we added, and not the default site we blocked
  211. assert.lengthOf(result, 1);
  212. assert.deepEqual(result[0], topsite);
  213. assert.notInclude(result, blockedDefaultSite);
  214. });
  215. it("should call dedupe on the links", async () => {
  216. const stub = sinon.stub(feed.dedupe, "group").callsFake((...id) => id);
  217. await feed.getLinksWithDefaults();
  218. assert.calledOnce(stub);
  219. });
  220. it("should dedupe the links by hostname", async () => {
  221. const site = {url: "foo", hostname: "bar"};
  222. const result = feed._dedupeKey(site);
  223. assert.equal(result, site.hostname);
  224. });
  225. it("should add defaults if there are are not enough links", async () => {
  226. links = [{frecency: FAKE_FRECENCY, url: "foo.com"}];
  227. const result = await feed.getLinksWithDefaults();
  228. const reference = [...links, ...DEFAULT_TOP_SITES].map(s =>
  229. Object.assign({}, s, {
  230. hostname: shortURLStub(s),
  231. typedBonus: true,
  232. }));
  233. assert.deepEqual(result, reference);
  234. });
  235. it("should only add defaults up to the number of visible slots", async () => {
  236. links = [];
  237. const numVisible = TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW;
  238. for (let i = 0; i < numVisible - 1; i++) {
  239. links.push({frecency: FAKE_FRECENCY, url: `foo${i}.com`});
  240. }
  241. const result = await feed.getLinksWithDefaults();
  242. const reference = [...links, DEFAULT_TOP_SITES[0]].map(s =>
  243. Object.assign({}, s, {
  244. hostname: shortURLStub(s),
  245. typedBonus: true,
  246. }));
  247. assert.lengthOf(result, numVisible);
  248. assert.deepEqual(result, reference);
  249. });
  250. it("should not throw if NewTabUtils returns null", () => {
  251. links = null;
  252. assert.doesNotThrow(() => {
  253. feed.getLinksWithDefaults();
  254. });
  255. });
  256. it("should get more if the user has asked for more", async () => {
  257. links = new Array(4 * TOP_SITES_MAX_SITES_PER_ROW).fill(null).map((v, i) => ({
  258. frecency: FAKE_FRECENCY,
  259. url: `http://www.site${i}.com`,
  260. }));
  261. feed.store.state.Prefs.values.topSitesRows = 3;
  262. const result = await feed.getLinksWithDefaults();
  263. assert.propertyVal(result, "length", feed.store.state.Prefs.values.topSitesRows * TOP_SITES_MAX_SITES_PER_ROW);
  264. });
  265. });
  266. describe("caching", () => {
  267. it("should reuse the cache on subsequent calls", async () => {
  268. await feed.getLinksWithDefaults();
  269. await feed.getLinksWithDefaults();
  270. assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites);
  271. });
  272. it("should ignore the cache when requesting more", async () => {
  273. await feed.getLinksWithDefaults();
  274. feed.store.state.Prefs.values.topSitesRows *= 3;
  275. await feed.getLinksWithDefaults();
  276. assert.calledTwice(global.NewTabUtils.activityStreamLinks.getTopSites);
  277. });
  278. it("should migrate frecent screenshot data without getting screenshots again", async () => {
  279. stubFaviconsToUseScreenshots();
  280. await feed.getLinksWithDefaults();
  281. const {callCount} = fakeScreenshot.getScreenshotForURL;
  282. feed.frecentCache.expire();
  283. const result = await feed.getLinksWithDefaults();
  284. assert.calledTwice(global.NewTabUtils.activityStreamLinks.getTopSites);
  285. assert.callCount(fakeScreenshot.getScreenshotForURL, callCount);
  286. assert.propertyVal(result[0], "screenshot", FAKE_SCREENSHOT);
  287. });
  288. it("should migrate pinned favicon data without getting favicons again", async () => {
  289. fakeNewTabUtils.pinnedLinks.links = [{url: "https://foo.com/"}];
  290. await feed.getLinksWithDefaults();
  291. const {callCount} = fakeNewTabUtils.activityStreamProvider._addFavicons;
  292. feed.pinnedCache.expire();
  293. const result = await feed.getLinksWithDefaults();
  294. assert.callCount(fakeNewTabUtils.activityStreamProvider._addFavicons, callCount);
  295. assert.propertyVal(result[0], "favicon", FAKE_FAVICON);
  296. assert.propertyVal(result[0], "faviconSize", FAKE_FAVICON_SIZE);
  297. });
  298. it("should not expose internal link properties", async () => {
  299. const result = await feed.getLinksWithDefaults();
  300. const internal = Object.keys(result[0]).filter(key => key.startsWith("__"));
  301. assert.equal(internal.join(""), "");
  302. });
  303. it("should copy the screenshot of the frecent site if pinned site doesn't have customScreenshotURL", async () => {
  304. links = [{url: "https://foo.com/", screenshot: "screenshot"}];
  305. fakeNewTabUtils.pinnedLinks.links = [{url: "https://foo.com/"}];
  306. const result = await feed.getLinksWithDefaults();
  307. assert.equal(result[0].screenshot, links[0].screenshot);
  308. });
  309. it("should not copy the frecent screenshot if customScreenshotURL is set", async () => {
  310. links = [{url: "https://foo.com/", screenshot: "screenshot"}];
  311. fakeNewTabUtils.pinnedLinks.links = [{url: "https://foo.com/", customScreenshotURL: "custom"}];
  312. const result = await feed.getLinksWithDefaults();
  313. assert.isUndefined(result[0].screenshot);
  314. });
  315. it("should keep the same screenshot if no frecent site is found", async () => {
  316. links = [];
  317. fakeNewTabUtils.pinnedLinks.links = [{url: "https://foo.com/", screenshot: "custom"}];
  318. const result = await feed.getLinksWithDefaults();
  319. assert.equal(result[0].screenshot, "custom");
  320. });
  321. it("should not overwrite pinned site screenshot", async () => {
  322. links = [{url: "https://foo.com/", screenshot: "foo"}];
  323. fakeNewTabUtils.pinnedLinks.links = [{url: "https://foo.com/", screenshot: "bar"}];
  324. const result = await feed.getLinksWithDefaults();
  325. assert.equal(result[0].screenshot, "bar");
  326. });
  327. it("should not set searchTopSite from frecent site", async () => {
  328. links = [{url: "https://foo.com/", searchTopSite: true, screenshot: "screenshot"}];
  329. fakeNewTabUtils.pinnedLinks.links = [{url: "https://foo.com/"}];
  330. const result = await feed.getLinksWithDefaults();
  331. assert.propertyVal(result[0], "searchTopSite", false);
  332. // But it should copy over other properties
  333. assert.propertyVal(result[0], "screenshot", "screenshot");
  334. });
  335. describe("concurrency", () => {
  336. beforeEach(() => {
  337. stubFaviconsToUseScreenshots();
  338. fakeScreenshot.getScreenshotForURL = sandbox.stub().resolves(FAKE_SCREENSHOT);
  339. });
  340. afterEach(() => {
  341. sandbox.restore();
  342. });
  343. const getTwice = () => Promise.all([feed.getLinksWithDefaults(), feed.getLinksWithDefaults()]);
  344. it("should call the backing data once", async () => {
  345. await getTwice();
  346. assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites);
  347. });
  348. it("should get screenshots once per link", async () => {
  349. await getTwice();
  350. assert.callCount(fakeScreenshot.getScreenshotForURL, FAKE_LINKS.length);
  351. });
  352. it("should dispatch once per link screenshot fetched", async () => {
  353. feed._requestRichIcon = sinon.stub();
  354. await getTwice();
  355. assert.callCount(feed.store.dispatch, FAKE_LINKS.length);
  356. });
  357. });
  358. });
  359. describe("deduping", () => {
  360. beforeEach(() => {
  361. ({TopSitesFeed, DEFAULT_TOP_SITES} = injector({
  362. "lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs},
  363. "common/Reducers.jsm": {insertPinned, TOP_SITES_DEFAULT_ROWS, TOP_SITES_MAX_SITES_PER_ROW},
  364. "lib/Screenshots.jsm": {Screenshots: fakeScreenshot},
  365. }));
  366. sandbox.stub(global.Services.eTLD, "getPublicSuffix").returns("com");
  367. feed = Object.assign(new TopSitesFeed(), {store: feed.store});
  368. });
  369. it("should not dedupe pinned sites", async () => {
  370. fakeNewTabUtils.pinnedLinks.links = [
  371. {url: "https://developer.mozilla.org/en-US/docs/Web"},
  372. {url: "https://developer.mozilla.org/en-US/docs/Learn"},
  373. ];
  374. const sites = await feed.getLinksWithDefaults();
  375. assert.lengthOf(sites, 2 * TOP_SITES_MAX_SITES_PER_ROW);
  376. assert.equal(sites[0].url, fakeNewTabUtils.pinnedLinks.links[0].url);
  377. assert.equal(sites[1].url, fakeNewTabUtils.pinnedLinks.links[1].url);
  378. assert.equal(sites[0].hostname, sites[1].hostname);
  379. });
  380. it("should prefer pinned sites over links", async () => {
  381. fakeNewTabUtils.pinnedLinks.links = [
  382. {url: "https://developer.mozilla.org/en-US/docs/Web"},
  383. {url: "https://developer.mozilla.org/en-US/docs/Learn"},
  384. ];
  385. // These will be the frecent results.
  386. links = [
  387. {frecency: FAKE_FRECENCY, url: "https://developer.mozilla.org/"},
  388. {frecency: FAKE_FRECENCY, url: "https://www.mozilla.org/"},
  389. ];
  390. const sites = await feed.getLinksWithDefaults();
  391. // Expecting 3 links where there's 2 pinned and 1 www.mozilla.org, so
  392. // the frecent with matching hostname as pinned is removed.
  393. assert.lengthOf(sites, 3);
  394. assert.equal(sites[0].url, fakeNewTabUtils.pinnedLinks.links[0].url);
  395. assert.equal(sites[1].url, fakeNewTabUtils.pinnedLinks.links[1].url);
  396. assert.equal(sites[2].url, links[1].url);
  397. });
  398. it("should return sites that have a title", async () => {
  399. // Simulate a pinned link with no title.
  400. fakeNewTabUtils.pinnedLinks.links = [{url: "https://github.com/mozilla/activity-stream"}];
  401. const sites = await feed.getLinksWithDefaults();
  402. for (const site of sites) {
  403. assert.isDefined(site.hostname);
  404. }
  405. });
  406. it("should check against null entries", async () => {
  407. fakeNewTabUtils.pinnedLinks.links = [null];
  408. await feed.getLinksWithDefaults();
  409. });
  410. });
  411. it("should call _fetchIcon for each link", async () => {
  412. sinon.spy(feed, "_fetchIcon");
  413. const results = await feed.getLinksWithDefaults();
  414. assert.callCount(feed._fetchIcon, results.length);
  415. results.forEach(link => {
  416. assert.calledWith(feed._fetchIcon, link);
  417. });
  418. });
  419. it("should call _fetchScreenshot when customScreenshotURL is set", async () => {
  420. links = [];
  421. fakeNewTabUtils.pinnedLinks.links = [{url: "https://foo.com", customScreenshotURL: "custom"}];
  422. sinon.stub(feed, "_fetchScreenshot");
  423. await feed.getLinksWithDefaults();
  424. assert.calledWith(feed._fetchScreenshot, sinon.match.object, "custom");
  425. });
  426. });
  427. describe("#init", () => {
  428. it("should call refresh (broadcast:true)", async () => {
  429. sandbox.stub(feed, "refresh");
  430. await feed.init();
  431. assert.calledOnce(feed.refresh);
  432. assert.calledWithExactly(feed.refresh, {broadcast: true});
  433. });
  434. it("should initialise the storage", async () => {
  435. await feed.init();
  436. assert.calledOnce(feed.store.dbStorage.getDbTable);
  437. assert.calledWithExactly(feed.store.dbStorage.getDbTable, "sectionPrefs");
  438. });
  439. });
  440. describe("#refresh", () => {
  441. beforeEach(() => {
  442. sandbox.stub(feed, "_fetchIcon");
  443. });
  444. it("should wait for tippytop to initialize", async () => {
  445. feed._tippyTopProvider.initialized = false;
  446. sinon.stub(feed._tippyTopProvider, "init").resolves();
  447. await feed.refresh();
  448. assert.calledOnce(feed._tippyTopProvider.init);
  449. });
  450. it("should not init the tippyTopProvider if already initialized", async () => {
  451. feed._tippyTopProvider.initialized = true;
  452. sinon.stub(feed._tippyTopProvider, "init").resolves();
  453. await feed.refresh();
  454. assert.notCalled(feed._tippyTopProvider.init);
  455. });
  456. it("should broadcast TOP_SITES_UPDATED", async () => {
  457. sinon.stub(feed, "getLinksWithDefaults").returns(Promise.resolve([]));
  458. await feed.refresh({broadcast: true});
  459. assert.calledOnce(feed.store.dispatch);
  460. assert.calledWithExactly(feed.store.dispatch, ac.BroadcastToContent({
  461. type: at.TOP_SITES_UPDATED,
  462. data: {links: [], pref: {collapsed: false}},
  463. }));
  464. });
  465. it("should dispatch an action with the links returned", async () => {
  466. await feed.refresh({broadcast: true});
  467. const reference = links.map(site => Object.assign({}, site, {
  468. hostname: shortURLStub(site),
  469. typedBonus: true,
  470. }));
  471. assert.calledOnce(feed.store.dispatch);
  472. assert.propertyVal(feed.store.dispatch.firstCall.args[0], "type", at.TOP_SITES_UPDATED);
  473. assert.deepEqual(feed.store.dispatch.firstCall.args[0].data.links, reference);
  474. });
  475. it("should handle empty slots in the resulting top sites array", async () => {
  476. links = [FAKE_LINKS[0]];
  477. fakeNewTabUtils.pinnedLinks.links = [null, null, FAKE_LINKS[1], null, null, null, null, null, FAKE_LINKS[2]];
  478. await feed.refresh({broadcast: true});
  479. assert.calledOnce(feed.store.dispatch);
  480. });
  481. it("should dispatch AlsoToPreloaded when broadcast is false", async () => {
  482. sandbox.stub(feed, "getLinksWithDefaults").returns([]);
  483. await feed.refresh({broadcast: false});
  484. assert.calledOnce(feed.store.dispatch);
  485. assert.calledWithExactly(feed.store.dispatch, ac.AlsoToPreloaded({
  486. type: at.TOP_SITES_UPDATED,
  487. data: {links: [], pref: {collapsed: false}},
  488. }));
  489. });
  490. it("should not init storage if it is already initialized", async () => {
  491. feed._storage.initialized = true;
  492. await feed.refresh({broadcast: false});
  493. assert.notCalled(feed._storage.init);
  494. });
  495. it("should catch indexedDB errors", async () => {
  496. feed._storage.get.throws(new Error());
  497. globals.sandbox.spy(global.Cu, "reportError");
  498. try {
  499. await feed.refresh({broadcast: false});
  500. } catch (e) {
  501. assert.fails();
  502. }
  503. assert.calledOnce(Cu.reportError);
  504. });
  505. });
  506. describe("#updateSectionPrefs", () => {
  507. it("should call updateSectionPrefs on UPDATE_SECTION_PREFS", () => {
  508. sandbox.stub(feed, "updateSectionPrefs");
  509. feed.onAction({type: at.UPDATE_SECTION_PREFS, data: {id: "topsites"}});
  510. assert.calledOnce(feed.updateSectionPrefs);
  511. });
  512. it("should dispatch TOP_SITES_PREFS_UPDATED", async () => {
  513. await feed.updateSectionPrefs({collapsed: true});
  514. assert.calledOnce(feed.store.dispatch);
  515. assert.calledWithExactly(feed.store.dispatch, ac.BroadcastToContent({
  516. type: at.TOP_SITES_PREFS_UPDATED,
  517. data: {pref: {collapsed: true}},
  518. }));
  519. });
  520. });
  521. describe("#getScreenshotPreview", () => {
  522. it("should dispatch preview if request is succesful", async () => {
  523. await feed.getScreenshotPreview("custom", 1234);
  524. assert.calledOnce(feed.store.dispatch);
  525. assert.calledWithExactly(feed.store.dispatch, ac.OnlyToOneContent({
  526. data: {preview: FAKE_SCREENSHOT, url: "custom"},
  527. type: at.PREVIEW_RESPONSE,
  528. }, 1234));
  529. });
  530. it("should return empty string if request fails", async () => {
  531. fakeScreenshot.getScreenshotForURL = sandbox.stub().returns(Promise.resolve(null));
  532. await feed.getScreenshotPreview("custom", 1234);
  533. assert.calledOnce(feed.store.dispatch);
  534. assert.calledWithExactly(feed.store.dispatch, ac.OnlyToOneContent({
  535. data: {preview: "", url: "custom"},
  536. type: at.PREVIEW_RESPONSE,
  537. }, 1234));
  538. });
  539. });
  540. describe("#_fetchIcon", () => {
  541. it("should reuse screenshot on the link", () => {
  542. const link = {screenshot: "reuse.png"};
  543. feed._fetchIcon(link);
  544. assert.notCalled(fakeScreenshot.getScreenshotForURL);
  545. assert.propertyVal(link, "screenshot", "reuse.png");
  546. });
  547. it("should reuse existing fetching screenshot on the link", async () => {
  548. const link = {__sharedCache: {fetchingScreenshot: Promise.resolve("fetching.png")}};
  549. await feed._fetchIcon(link);
  550. assert.notCalled(fakeScreenshot.getScreenshotForURL);
  551. });
  552. it("should get a screenshot if the link is missing it", () => {
  553. feed._fetchIcon(Object.assign({__sharedCache: {}}, FAKE_LINKS[0]));
  554. assert.calledOnce(fakeScreenshot.getScreenshotForURL);
  555. assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_LINKS[0].url);
  556. });
  557. it("should update the link's cache with a screenshot", async () => {
  558. const updateLink = sandbox.stub();
  559. const link = {__sharedCache: {updateLink}};
  560. await feed._fetchIcon(link);
  561. assert.calledOnce(updateLink);
  562. assert.calledWith(updateLink, "screenshot", FAKE_SCREENSHOT);
  563. });
  564. it("should skip getting a screenshot if there is a tippy top icon", () => {
  565. feed._tippyTopProvider.processSite = site => {
  566. site.tippyTopIcon = "icon.png";
  567. site.backgroundColor = "#fff";
  568. return site;
  569. };
  570. const link = {url: "example.com"};
  571. feed._fetchIcon(link);
  572. assert.propertyVal(link, "tippyTopIcon", "icon.png");
  573. assert.notProperty(link, "screenshot");
  574. assert.notCalled(fakeScreenshot.getScreenshotForURL);
  575. });
  576. it("should skip getting a screenshot if there is an icon of size greater than 96x96 and no tippy top", () => {
  577. const link = {
  578. url: "foo.com",
  579. favicon: "data:foo",
  580. faviconSize: 196,
  581. };
  582. feed._fetchIcon(link);
  583. assert.notProperty(link, "tippyTopIcon");
  584. assert.notProperty(link, "screenshot");
  585. assert.notCalled(fakeScreenshot.getScreenshotForURL);
  586. });
  587. it("should use the link's rich icon even if there's a tippy top", () => {
  588. feed._tippyTopProvider.processSite = site => {
  589. site.tippyTopIcon = "icon.png";
  590. site.backgroundColor = "#fff";
  591. return site;
  592. };
  593. const link = {
  594. url: "foo.com",
  595. favicon: "data:foo",
  596. faviconSize: 196,
  597. };
  598. feed._fetchIcon(link);
  599. assert.notProperty(link, "tippyTopIcon");
  600. });
  601. });
  602. describe("#_fetchScreenshot", () => {
  603. it("should call maybeCacheScreenshot", async () => {
  604. const updateLink = sinon.stub();
  605. const link = {customScreenshotURL: "custom", __sharedCache: {updateLink}};
  606. await feed._fetchScreenshot(link, "custom");
  607. assert.calledOnce(fakeScreenshot.maybeCacheScreenshot);
  608. assert.calledWithExactly(fakeScreenshot.maybeCacheScreenshot, link, link.customScreenshotURL,
  609. "screenshot", sinon.match.func);
  610. });
  611. it("should not call maybeCacheScreenshot if screenshot is set", async () => {
  612. const updateLink = sinon.stub();
  613. const link = {customScreenshotURL: "custom", __sharedCache: {updateLink}, screenshot: true};
  614. await feed._fetchScreenshot(link, "custom");
  615. assert.notCalled(fakeScreenshot.maybeCacheScreenshot);
  616. });
  617. });
  618. describe("#onAction", () => {
  619. it("should call getScreenshotPreview on PREVIEW_REQUEST", () => {
  620. sandbox.stub(feed, "getScreenshotPreview");
  621. feed.onAction({type: at.PREVIEW_REQUEST, data: {url: "foo"}, meta: {fromTarget: 1234}});
  622. assert.calledOnce(feed.getScreenshotPreview);
  623. assert.calledWithExactly(feed.getScreenshotPreview, "foo", 1234);
  624. });
  625. it("should refresh on SYSTEM_TICK", async () => {
  626. sandbox.stub(feed, "refresh");
  627. feed.onAction({type: at.SYSTEM_TICK});
  628. assert.calledOnce(feed.refresh);
  629. assert.calledWithExactly(feed.refresh, {broadcast: false});
  630. });
  631. it("should call with correct parameters on TOP_SITES_PIN", () => {
  632. const pinAction = {
  633. type: at.TOP_SITES_PIN,
  634. data: {site: {url: "foo.com"}, index: 7},
  635. };
  636. feed.onAction(pinAction);
  637. assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
  638. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, pinAction.data.site, pinAction.data.index);
  639. });
  640. it("should call pin on TOP_SITES_PIN", () => {
  641. sinon.stub(feed, "pin");
  642. const pinExistingAction = {type: at.TOP_SITES_PIN, data: {site: FAKE_LINKS[4], index: 4}};
  643. feed.onAction(pinExistingAction);
  644. assert.calledOnce(feed.pin);
  645. });
  646. it("should trigger refresh on TOP_SITES_PIN", async () => {
  647. sinon.stub(feed, "refresh");
  648. const pinExistingAction = {type: at.TOP_SITES_PIN, data: {site: FAKE_LINKS[4], index: 4}};
  649. await feed.pin(pinExistingAction);
  650. assert.calledOnce(feed.refresh);
  651. });
  652. it("should unblock a previously blocked top site if we are now adding it manually via 'Add a Top Site' option", async () => {
  653. const pinAction = {
  654. type: at.TOP_SITES_PIN,
  655. data: {site: {url: "foo.com"}, index: -1},
  656. };
  657. feed.onAction(pinAction);
  658. assert.calledWith(fakeNewTabUtils.blockedLinks.unblock, {url: pinAction.data.site.url});
  659. });
  660. it("should call insert on TOP_SITES_INSERT", async () => {
  661. sinon.stub(feed, "insert");
  662. const addAction = {type: at.TOP_SITES_INSERT, data: {site: {url: "foo.com"}}};
  663. feed.onAction(addAction);
  664. assert.calledOnce(feed.insert);
  665. });
  666. it("should trigger refresh on TOP_SITES_INSERT", async () => {
  667. sinon.stub(feed, "refresh");
  668. const addAction = {type: at.TOP_SITES_INSERT, data: {site: {url: "foo.com"}}};
  669. await feed.insert(addAction);
  670. assert.calledOnce(feed.refresh);
  671. });
  672. it("should call unpin with correct parameters on TOP_SITES_UNPIN", () => {
  673. fakeNewTabUtils.pinnedLinks.links = [null, null, {url: "foo.com"}, null, null, null, null, null, FAKE_LINKS[0]];
  674. const unpinAction = {
  675. type: at.TOP_SITES_UNPIN,
  676. data: {site: {url: "foo.com"}},
  677. };
  678. feed.onAction(unpinAction);
  679. assert.calledOnce(fakeNewTabUtils.pinnedLinks.unpin);
  680. assert.calledWith(fakeNewTabUtils.pinnedLinks.unpin, unpinAction.data.site);
  681. });
  682. it("should call refresh without a target if we clear history with PLACES_HISTORY_CLEARED", () => {
  683. sandbox.stub(feed, "refresh");
  684. feed.onAction({type: at.PLACES_HISTORY_CLEARED});
  685. assert.calledOnce(feed.refresh);
  686. assert.calledWithExactly(feed.refresh, {broadcast: true});
  687. });
  688. it("should call refresh without a target if we remove a Topsite from history", () => {
  689. sandbox.stub(feed, "refresh");
  690. feed.onAction({type: at.PLACES_LINK_DELETED});
  691. assert.calledOnce(feed.refresh);
  692. assert.calledWithExactly(feed.refresh, {broadcast: true});
  693. });
  694. it("should still dispatch an action even if there's no target provided", async () => {
  695. sandbox.stub(feed, "_fetchIcon");
  696. await feed.refresh({broadcast: true});
  697. assert.calledOnce(feed.store.dispatch);
  698. assert.propertyVal(feed.store.dispatch.firstCall.args[0], "type", at.TOP_SITES_UPDATED);
  699. });
  700. it("should call init on INIT action", async () => {
  701. sinon.stub(feed, "init");
  702. feed.onAction({type: at.INIT});
  703. assert.calledOnce(feed.init);
  704. });
  705. it("should call refresh on PLACES_LINK_BLOCKED action", async () => {
  706. sinon.stub(feed, "refresh");
  707. await feed.onAction({type: at.PLACES_LINK_BLOCKED});
  708. assert.calledOnce(feed.refresh);
  709. assert.calledWithExactly(feed.refresh, {broadcast: true});
  710. });
  711. it("should call refresh on PLACES_LINKS_CHANGED action", async () => {
  712. sinon.stub(feed, "refresh");
  713. await feed.onAction({type: at.PLACES_LINKS_CHANGED});
  714. assert.calledOnce(feed.refresh);
  715. assert.calledWithExactly(feed.refresh, {broadcast: false});
  716. });
  717. it("should call pin with correct args on TOP_SITES_INSERT without an index specified", () => {
  718. const addAction = {
  719. type: at.TOP_SITES_INSERT,
  720. data: {site: {url: "foo.bar", label: "foo"}},
  721. };
  722. feed.onAction(addAction);
  723. assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
  724. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, addAction.data.site, 0);
  725. });
  726. it("should call pin with correct args on TOP_SITES_INSERT", () => {
  727. const dropAction = {
  728. type: at.TOP_SITES_INSERT,
  729. data: {site: {url: "foo.bar", label: "foo"}, index: 3},
  730. };
  731. feed.onAction(dropAction);
  732. assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
  733. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, dropAction.data.site, 3);
  734. });
  735. it("should remove the expiration filter on UNINIT", () => {
  736. feed.onAction({type: "UNINIT"});
  737. assert.calledOnce(fakePageThumbs.removeExpirationFilter);
  738. });
  739. it("should call updatePinnedSearchShortcuts on UPDATE_PINNED_SEARCH_SHORTCUTS action", async () => {
  740. sinon.stub(feed, "updatePinnedSearchShortcuts");
  741. const addedShortcuts = [{url: "https://google.com", searchVendor: "google", label: "google", searchTopSite: true}];
  742. await feed.onAction({type: at.UPDATE_PINNED_SEARCH_SHORTCUTS, data: {addedShortcuts}});
  743. assert.calledOnce(feed.updatePinnedSearchShortcuts);
  744. });
  745. });
  746. describe("#add", () => {
  747. it("should pin site in first slot of empty pinned list", () => {
  748. const site = {url: "foo.bar", label: "foo"};
  749. feed.insert({data: {site}});
  750. assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
  751. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);
  752. });
  753. it("should pin site in first slot of pinned list with empty first slot", () => {
  754. fakeNewTabUtils.pinnedLinks.links = [null, {url: "example.com"}];
  755. const site = {url: "foo.bar", label: "foo"};
  756. feed.insert({data: {site}});
  757. assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
  758. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);
  759. });
  760. it("should move a pinned site in first slot to the next slot: part 1", () => {
  761. const site1 = {url: "example.com"};
  762. fakeNewTabUtils.pinnedLinks.links = [site1];
  763. const site = {url: "foo.bar", label: "foo"};
  764. feed.insert({data: {site}});
  765. assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin);
  766. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);
  767. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 1);
  768. });
  769. it("should move a pinned site in first slot to the next slot: part 2", () => {
  770. const site1 = {url: "example.com"};
  771. const site2 = {url: "example.org"};
  772. fakeNewTabUtils.pinnedLinks.links = [site1, null, site2];
  773. const site = {url: "foo.bar", label: "foo"};
  774. feed.insert({data: {site}});
  775. assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin);
  776. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);
  777. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 1);
  778. });
  779. it("should unpin the last site if all slots are already pinned", () => {
  780. const site1 = {url: "example.com"};
  781. const site2 = {url: "example.org"};
  782. const site3 = {url: "example.net"};
  783. const site4 = {url: "example.biz"};
  784. const site5 = {url: "example.info"};
  785. const site6 = {url: "example.news"};
  786. const site7 = {url: "example.lol"};
  787. const site8 = {url: "example.golf"};
  788. fakeNewTabUtils.pinnedLinks.links = [site1, site2, site3, site4, site5, site6, site7, site8];
  789. feed.store.state.Prefs.values.topSitesRows = 1;
  790. const site = {url: "foo.bar", label: "foo"};
  791. feed.insert({data: {site}});
  792. assert.equal(fakeNewTabUtils.pinnedLinks.pin.callCount, 8);
  793. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);
  794. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 1);
  795. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site2, 2);
  796. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site3, 3);
  797. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site4, 4);
  798. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site5, 5);
  799. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site6, 6);
  800. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site7, 7);
  801. });
  802. });
  803. describe("#pin", () => {
  804. it("should pin site in specified slot empty pinned list", async () => {
  805. const site = {url: "foo.bar", label: "foo", customScreenshotURL: "screenshot"};
  806. await feed.pin({data: {index: 2, site}});
  807. assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
  808. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2);
  809. });
  810. it("should lookup the link object to update the custom screenshot", async () => {
  811. const site = {url: "foo.bar", label: "foo", customScreenshotURL: "screenshot"};
  812. sandbox.spy(feed.pinnedCache, "request");
  813. await feed.pin({data: {index: 2, site}});
  814. assert.calledOnce(feed.pinnedCache.request);
  815. });
  816. it("should lookup the link object to update the custom screenshot", async () => {
  817. const site = {url: "foo.bar", label: "foo", customScreenshotURL: null};
  818. sandbox.spy(feed.pinnedCache, "request");
  819. await feed.pin({data: {index: 2, site}});
  820. assert.calledOnce(feed.pinnedCache.request);
  821. });
  822. it("should not do a link object lookup if custom screenshot field is not set", async () => {
  823. const site = {url: "foo.bar", label: "foo"};
  824. sandbox.spy(feed.pinnedCache, "request");
  825. await feed.pin({data: {index: 2, site}});
  826. assert.notCalled(feed.pinnedCache.request);
  827. });
  828. it("should pin site in specified slot of pinned list that is free", () => {
  829. fakeNewTabUtils.pinnedLinks.links = [null, {url: "example.com"}];
  830. const site = {url: "foo.bar", label: "foo"};
  831. feed.pin({data: {index: 2, site}});
  832. assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
  833. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2);
  834. });
  835. it("should save the searchTopSite attribute if set", () => {
  836. fakeNewTabUtils.pinnedLinks.links = [null, {url: "example.com"}];
  837. const site = {url: "foo.bar", label: "foo", searchTopSite: true};
  838. feed.pin({data: {index: 2, site}});
  839. assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
  840. assert.propertyVal(fakeNewTabUtils.pinnedLinks.pin.firstCall.args[0], "searchTopSite", true);
  841. });
  842. it("should NOT move a pinned site in specified slot to the next slot", () => {
  843. fakeNewTabUtils.pinnedLinks.links = [null, null, {url: "example.com"}];
  844. const site = {url: "foo.bar", label: "foo"};
  845. feed.pin({data: {index: 2, site}});
  846. assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
  847. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2);
  848. });
  849. it("should properly update LinksCache object properties between migrations", async () => {
  850. fakeNewTabUtils.pinnedLinks.links = [{url: "https://foo.com/"}];
  851. let pinnedLinks = await feed.pinnedCache.request();
  852. assert.equal(pinnedLinks.length, 1);
  853. feed.pinnedCache.expire();
  854. pinnedLinks[0].__sharedCache.updateLink("screenshot", "foo");
  855. pinnedLinks = await feed.pinnedCache.request();
  856. assert.propertyVal(pinnedLinks[0], "screenshot", "foo");
  857. // Force cache expiration in order to trigger a migration of objects
  858. feed.pinnedCache.expire();
  859. pinnedLinks[0].__sharedCache.updateLink("screenshot", "bar");
  860. pinnedLinks = await feed.pinnedCache.request();
  861. assert.propertyVal(pinnedLinks[0], "screenshot", "bar");
  862. });
  863. it("should call insert if index < 0", () => {
  864. const site = {url: "foo.bar", label: "foo"};
  865. const action = {data: {index: -1, site}};
  866. sandbox.spy(feed, "insert");
  867. feed.pin(action);
  868. assert.calledOnce(feed.insert);
  869. assert.calledWithExactly(feed.insert, action);
  870. });
  871. it("should not call insert if index == 0", () => {
  872. const site = {url: "foo.bar", label: "foo"};
  873. const action = {data: {index: 0, site}};
  874. sandbox.spy(feed, "insert");
  875. feed.pin(action);
  876. assert.notCalled(feed.insert);
  877. });
  878. });
  879. describe("clearLinkCustomScreenshot", () => {
  880. it("should remove cached screenshot if custom url changes", async () => {
  881. const stub = sandbox.stub();
  882. sandbox.stub(feed.pinnedCache, "request").returns(Promise.resolve([{
  883. url: "foo",
  884. customScreenshotURL: "old_screenshot",
  885. __sharedCache: {updateLink: stub},
  886. }]));
  887. await feed._clearLinkCustomScreenshot({url: "foo", customScreenshotURL: "new_screenshot"});
  888. assert.calledOnce(stub);
  889. assert.calledWithExactly(stub, "screenshot", undefined);
  890. });
  891. it("should remove cached screenshot if custom url is removed", async () => {
  892. const stub = sandbox.stub();
  893. sandbox.stub(feed.pinnedCache, "request").returns(Promise.resolve([{
  894. url: "foo",
  895. customScreenshotURL: "old_screenshot",
  896. __sharedCache: {updateLink: stub},
  897. }]));
  898. await feed._clearLinkCustomScreenshot({url: "foo", customScreenshotURL: "new_screenshot"});
  899. assert.calledOnce(stub);
  900. assert.calledWithExactly(stub, "screenshot", undefined);
  901. });
  902. });
  903. describe("#drop", () => {
  904. it("should correctly handle different index values", () => {
  905. let index = -1;
  906. const site = {url: "foo.bar", label: "foo"};
  907. const action = {data: {index, site}};
  908. feed.insert(action);
  909. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);
  910. index = undefined;
  911. feed.insert(action);
  912. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);
  913. });
  914. it("should pin site in specified slot that is free", () => {
  915. fakeNewTabUtils.pinnedLinks.links = [null, {url: "example.com"}];
  916. const site = {url: "foo.bar", label: "foo"};
  917. feed.insert({data: {index: 2, site, draggedFromIndex: 0}});
  918. assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
  919. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2);
  920. });
  921. it("should move a pinned site in specified slot to the next slot", () => {
  922. fakeNewTabUtils.pinnedLinks.links = [null, null, {url: "example.com"}];
  923. const site = {url: "foo.bar", label: "foo"};
  924. feed.insert({data: {index: 2, site, draggedFromIndex: 3}});
  925. assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin);
  926. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2);
  927. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, {url: "example.com"}, 3);
  928. });
  929. it("should move pinned sites in the direction of the dragged site", () => {
  930. const site1 = {url: "foo.bar", label: "foo"};
  931. const site2 = {url: "example.com", label: "example"};
  932. fakeNewTabUtils.pinnedLinks.links = [null, null, site2];
  933. feed.insert({data: {index: 2, site: site1, draggedFromIndex: 0}});
  934. assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin);
  935. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 2);
  936. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site2, 1);
  937. fakeNewTabUtils.pinnedLinks.pin.resetHistory();
  938. feed.insert({data: {index: 2, site: site1, draggedFromIndex: 5}});
  939. assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin);
  940. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 2);
  941. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site2, 3);
  942. });
  943. it("should not insert past the visible top sites", () => {
  944. const site1 = {url: "foo.bar", label: "foo"};
  945. feed.insert({data: {index: 42, site: site1, draggedFromIndex: 0}});
  946. assert.notCalled(fakeNewTabUtils.pinnedLinks.pin);
  947. });
  948. });
  949. describe("integration", () => {
  950. let resolvers = [];
  951. beforeEach(() => {
  952. feed.store.dispatch = sandbox.stub().callsFake(() => {
  953. resolvers.shift()();
  954. });
  955. sandbox.stub(feed, "_fetchScreenshot");
  956. });
  957. afterEach(() => {
  958. sandbox.restore();
  959. });
  960. const forDispatch = action => new Promise(resolve => {
  961. resolvers.push(resolve);
  962. feed.onAction(action);
  963. });
  964. it("should add a pinned site and remove it", async () => {
  965. feed._requestRichIcon = sinon.stub();
  966. const url = "https://pin.me";
  967. fakeNewTabUtils.pinnedLinks.pin = sandbox.stub().callsFake(link => {
  968. fakeNewTabUtils.pinnedLinks.links.push(link);
  969. });
  970. await forDispatch({type: at.TOP_SITES_INSERT, data: {site: {url}}});
  971. fakeNewTabUtils.pinnedLinks.links.pop();
  972. await forDispatch({type: at.PLACES_LINK_BLOCKED});
  973. assert.calledTwice(feed.store.dispatch);
  974. assert.equal(feed.store.dispatch.firstCall.args[0].data.links[0].url, url);
  975. assert.equal(feed.store.dispatch.secondCall.args[0].data.links[0].url, FAKE_LINKS[0].url);
  976. });
  977. });
  978. describe("improvesearch.noDefaultSearchTile experiment", () => {
  979. const NO_DEFAULT_SEARCH_TILE_PREF = "improvesearch.noDefaultSearchTile";
  980. beforeEach(() => {
  981. global.Services.search.getDefault = async () => ({identifier: "google", searchForm: "google.com"});
  982. feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
  983. });
  984. it("should filter out alexa top 5 search from the default sites", async () => {
  985. const TOP_5_TEST = [
  986. "google.com",
  987. "search.yahoo.com",
  988. "yahoo.com",
  989. "bing.com",
  990. "ask.com",
  991. "duckduckgo.com",
  992. ];
  993. links = [{url: "amazon.com"}, ...TOP_5_TEST.map(url => ({url}))];
  994. const urlsReturned = (await feed.getLinksWithDefaults()).map(link => link.url);
  995. assert.include(urlsReturned, "amazon.com");
  996. TOP_5_TEST.forEach(url => assert.notInclude(urlsReturned, url));
  997. });
  998. it("should not filter out alexa, default search from the query results if the experiment pref is off", async () => {
  999. links = [{url: "google.com"}, {url: "foo.com"}, {url: "duckduckgo"}];
  1000. feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = false;
  1001. const urlsReturned = (await feed.getLinksWithDefaults()).map(link => link.url);
  1002. assert.include(urlsReturned, "google.com");
  1003. });
  1004. it("should filter out the current default search from the default sites", async () => {
  1005. feed._currentSearchHostname = "amazon";
  1006. feed.onAction({type: at.PREFS_INITIAL_VALUES, data: {"default.sites": "google.com,amazon.com"}});
  1007. links = [{url: "foo.com"}];
  1008. const urlsReturned = (await feed.getLinksWithDefaults()).map(link => link.url);
  1009. assert.notInclude(urlsReturned, "amazon.com");
  1010. });
  1011. it("should not filter out current default search from pinned sites even if it matches the current default search", async () => {
  1012. links = [{url: "foo.com"}];
  1013. fakeNewTabUtils.pinnedLinks.links = [{url: "google.com"}];
  1014. const urlsReturned = (await feed.getLinksWithDefaults()).map(link => link.url);
  1015. assert.include(urlsReturned, "google.com");
  1016. });
  1017. it("should call refresh and set ._currentSearchHostname to the new engine hostname when the the default search engine has been set", () => {
  1018. sinon.stub(feed, "refresh");
  1019. sandbox.stub(global.Services.search, "defaultEngine").value({identifier: "ddg", searchForm: "duckduckgo.com"});
  1020. feed.observe(null, "browser-search-engine-modified", "engine-default");
  1021. assert.equal(feed._currentSearchHostname, "duckduckgo");
  1022. assert.calledOnce(feed.refresh);
  1023. });
  1024. it("should call refresh when the experiment pref has changed", () => {
  1025. sinon.stub(feed, "refresh");
  1026. feed.onAction({type: at.PREF_CHANGED, data: {name: NO_DEFAULT_SEARCH_TILE_PREF, value: true}});
  1027. assert.calledOnce(feed.refresh);
  1028. feed.onAction({type: at.PREF_CHANGED, data: {name: NO_DEFAULT_SEARCH_TILE_PREF, value: false}});
  1029. assert.calledTwice(feed.refresh);
  1030. });
  1031. });
  1032. describe("improvesearch.topSitesSearchShortcuts", () => {
  1033. beforeEach(() => {
  1034. feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = true;
  1035. feed.store.state.Prefs.values[SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF] = "google,amazon";
  1036. feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = "";
  1037. const searchEngines = [
  1038. {wrappedJSObject: {_internalAliases: ["@google"]}},
  1039. {wrappedJSObject: {_internalAliases: ["@amazon"]}},
  1040. ];
  1041. global.Services.search.getDefaultEngines = async () => searchEngines;
  1042. fakeNewTabUtils.pinnedLinks.pin = sinon.stub().callsFake((site, index) => {
  1043. fakeNewTabUtils.pinnedLinks.links[index] = site;
  1044. });
  1045. });
  1046. it("should properly disable search improvements if the pref is off", async () => {
  1047. sandbox.stub(global.Services.prefs, "clearUserPref");
  1048. sandbox.spy(feed.pinnedCache, "expire");
  1049. sandbox.spy(feed, "refresh");
  1050. // an actual implementation of unpin (until we can get a mochitest for search improvements)
  1051. fakeNewTabUtils.pinnedLinks.unpin = sinon.stub().callsFake(site => {
  1052. let index = -1;
  1053. for (let i = 0; i < fakeNewTabUtils.pinnedLinks.links.length; i++) {
  1054. let link = fakeNewTabUtils.pinnedLinks.links[i];
  1055. if (link && link.url === site.url) {
  1056. index = i;
  1057. }
  1058. }
  1059. if (index > -1) {
  1060. fakeNewTabUtils.pinnedLinks.links[index] = null;
  1061. }
  1062. });
  1063. // ensure we've inserted search shorcuts + pin an additional site in space 4
  1064. await feed._maybeInsertSearchShortcuts(fakeNewTabUtils.pinnedLinks.links);
  1065. fakeNewTabUtils.pinnedLinks.pin({url: "https://dontunpinme.com"}, 3);
  1066. // turn the experiment off
  1067. feed.onAction({type: at.PREF_CHANGED, data: {name: SEARCH_SHORTCUTS_EXPERIMENT_PREF, value: false}});
  1068. // check we cleared the pref, expired the pinned cache, and refreshed the feed
  1069. assert.calledWith(global.Services.prefs.clearUserPref, `browser.newtabpage.activity-stream.${SEARCH_SHORTCUTS_HAVE_PINNED_PREF}`);
  1070. assert.calledOnce(feed.pinnedCache.expire);
  1071. assert.calledWith(feed.refresh, {broadcast: true});
  1072. // check that the search shortcuts were removed from the list of pinned sites
  1073. const urlsReturned = fakeNewTabUtils.pinnedLinks.links.filter(s => s).map(link => link.url);
  1074. assert.notInclude(urlsReturned, "https://amazon.com");
  1075. assert.notInclude(urlsReturned, "https://google.com");
  1076. assert.include(urlsReturned, "https://dontunpinme.com");
  1077. // check that the positions where the search shortcuts were null, and the additional pinned site is untouched in space 4
  1078. assert.equal(fakeNewTabUtils.pinnedLinks.links[0], null);
  1079. assert.equal(fakeNewTabUtils.pinnedLinks.links[1], null);
  1080. assert.equal(fakeNewTabUtils.pinnedLinks.links[2], undefined);
  1081. assert.deepEqual(fakeNewTabUtils.pinnedLinks.links[3], {url: "https://dontunpinme.com"});
  1082. });
  1083. it("should updateCustomSearchShortcuts when experiment pref is turned on", async () => {
  1084. feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = false;
  1085. feed.updateCustomSearchShortcuts = sinon.spy();
  1086. // turn the experiment on
  1087. feed.onAction({type: at.PREF_CHANGED, data: {name: SEARCH_SHORTCUTS_EXPERIMENT_PREF, value: true}});
  1088. assert.calledOnce(feed.updateCustomSearchShortcuts);
  1089. });
  1090. it("should filter out default top sites that match a hostname of a search shortcut if previously blocked", async () => {
  1091. feed.refreshDefaults("https://amazon.ca");
  1092. fakeNewTabUtils.blockedLinks.links = [{url: "https://amazon.com"}];
  1093. fakeNewTabUtils.blockedLinks.isBlocked = site => (fakeNewTabUtils.blockedLinks.links[0].url === site.url);
  1094. const urlsReturned = (await feed.getLinksWithDefaults()).map(link => link.url);
  1095. assert.notInclude(urlsReturned, "https://amazon.ca");
  1096. });
  1097. it("should update frecent search topsite icon", async () => {
  1098. feed._tippyTopProvider.processSite = site => {
  1099. site.tippyTopIcon = "icon.png";
  1100. site.backgroundColor = "#fff";
  1101. return site;
  1102. };
  1103. links = [{url: "google.com"}];
  1104. const urlsReturned = await feed.getLinksWithDefaults();
  1105. const defaultSearchTopsite = urlsReturned.find(s => s.url === "google.com");
  1106. assert.propertyVal(defaultSearchTopsite, "searchTopSite", true);
  1107. assert.equal(defaultSearchTopsite.tippyTopIcon, "icon.png");
  1108. assert.equal(defaultSearchTopsite.backgroundColor, "#fff");
  1109. });
  1110. it("should update default search topsite icon", async () => {
  1111. feed._tippyTopProvider.processSite = site => {
  1112. site.tippyTopIcon = "icon.png";
  1113. site.backgroundColor = "#fff";
  1114. return site;
  1115. };
  1116. links = [{url: "foo.com"}];
  1117. feed.onAction({type: at.PREFS_INITIAL_VALUES, data: {"default.sites": "google.com,amazon.com"}});
  1118. const urlsReturned = await feed.getLinksWithDefaults();
  1119. const defaultSearchTopsite = urlsReturned.find(s => s.url === "amazon.com");
  1120. assert.propertyVal(defaultSearchTopsite, "searchTopSite", true);
  1121. assert.equal(defaultSearchTopsite.tippyTopIcon, "icon.png");
  1122. assert.equal(defaultSearchTopsite.backgroundColor, "#fff");
  1123. });
  1124. it("should dispatch UPDATE_SEARCH_SHORTCUTS on updateCustomSearchShortcuts", async () => {
  1125. feed.store.state.Prefs.values["improvesearch.noDefaultSearchTile"] = true;
  1126. await feed.updateCustomSearchShortcuts();
  1127. assert.calledOnce(feed.store.dispatch);
  1128. assert.calledWith(feed.store.dispatch, {
  1129. data: {
  1130. searchShortcuts: [{
  1131. keyword: "@google",
  1132. shortURL: "google",
  1133. url: "https://google.com",
  1134. }, {
  1135. keyword: "@amazon",
  1136. shortURL: "amazon",
  1137. url: "https://amazon.com",
  1138. }],
  1139. },
  1140. meta: {from: "ActivityStream:Main", to: "ActivityStream:Content"},
  1141. type: "UPDATE_SEARCH_SHORTCUTS",
  1142. });
  1143. });
  1144. describe("_maybeInsertSearchShortcuts", () => {
  1145. beforeEach(() => {
  1146. // Default is one row
  1147. feed.store.state.Prefs.values.topSitesRows = TOP_SITES_DEFAULT_ROWS;
  1148. // Eight slots per row
  1149. fakeNewTabUtils.pinnedLinks.links = [{url: ""}, {url: ""}, {url: ""}, null, {url: ""}, {url: ""}, null, {url: ""}];
  1150. });
  1151. it("should be called on getLinksWithDefaults", async () => {
  1152. sandbox.spy(feed, "_maybeInsertSearchShortcuts");
  1153. await feed.getLinksWithDefaults();
  1154. assert.calledOnce(feed._maybeInsertSearchShortcuts);
  1155. });
  1156. it("should do nothing and return false if the experiment is disabled", async () => {
  1157. feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = false;
  1158. assert.isFalse(await feed._maybeInsertSearchShortcuts(fakeNewTabUtils.pinnedLinks.links));
  1159. assert.notCalled(fakeNewTabUtils.pinnedLinks.pin);
  1160. });
  1161. it("should pin shortcuts in the correct order, into the available unpinned slots", async () => {
  1162. await feed._maybeInsertSearchShortcuts(fakeNewTabUtils.pinnedLinks.links);
  1163. // The shouldPin pref is "google,amazon" so expect the shortcuts in that order
  1164. assert.deepEqual(fakeNewTabUtils.pinnedLinks.links[3], {url: "https://google.com", searchTopSite: true, label: "@google"});
  1165. assert.deepEqual(fakeNewTabUtils.pinnedLinks.links[6], {url: "https://amazon.com", searchTopSite: true, label: "@amazon"});
  1166. });
  1167. it("should not pin shortcuts for the current default search engine", async () => {
  1168. feed._currentSearchHostname = "google";
  1169. await feed._maybeInsertSearchShortcuts(fakeNewTabUtils.pinnedLinks.links);
  1170. assert.deepEqual(fakeNewTabUtils.pinnedLinks.links[3], {url: "https://amazon.com", searchTopSite: true, label: "@amazon"});
  1171. });
  1172. it("should only pin the first shortcut if there's only one available slot", async () => {
  1173. fakeNewTabUtils.pinnedLinks.links[3] = {url: ""};
  1174. await feed._maybeInsertSearchShortcuts(fakeNewTabUtils.pinnedLinks.links);
  1175. // The first item in the shouldPin pref is "google" so expect only Google to be pinned
  1176. assert.ok(fakeNewTabUtils.pinnedLinks.links.find(s => s && s.url === "https://google.com"));
  1177. assert.notOk(fakeNewTabUtils.pinnedLinks.links.find(s => s && s.url === "https://amazon.com"));
  1178. });
  1179. it("should pin none if there's no available slot", async () => {
  1180. fakeNewTabUtils.pinnedLinks.links[3] = {url: ""};
  1181. fakeNewTabUtils.pinnedLinks.links[6] = {url: ""};
  1182. await feed._maybeInsertSearchShortcuts(fakeNewTabUtils.pinnedLinks.links);
  1183. assert.notOk(fakeNewTabUtils.pinnedLinks.links.find(s => s && s.url === "https://google.com"));
  1184. assert.notOk(fakeNewTabUtils.pinnedLinks.links.find(s => s && s.url === "https://amazon.com"));
  1185. });
  1186. it("should not pin a shortcut if the corresponding search engine is not available", async () => {
  1187. // Make Amazon search engine unavailable
  1188. global.Services.search.getDefaultEngines = async () => [{wrappedJSObject: {_internalAliases: ["@google"]}}];
  1189. fakeNewTabUtils.pinnedLinks.links.fill(null);
  1190. await feed._maybeInsertSearchShortcuts(fakeNewTabUtils.pinnedLinks.links);
  1191. assert.notOk(fakeNewTabUtils.pinnedLinks.links.find(s => s && s.url === "https://amazon.com"));
  1192. });
  1193. it("should not pin a search shortcut if it's been pinned before", async () => {
  1194. fakeNewTabUtils.pinnedLinks.links.fill(null);
  1195. feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = "google,amazon";
  1196. await feed._maybeInsertSearchShortcuts(fakeNewTabUtils.pinnedLinks.links);
  1197. assert.notOk(fakeNewTabUtils.pinnedLinks.links.find(s => s && s.url === "https://google.com"));
  1198. assert.notOk(fakeNewTabUtils.pinnedLinks.links.find(s => s && s.url === "https://amazon.com"));
  1199. fakeNewTabUtils.pinnedLinks.links.fill(null);
  1200. feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = "amazon";
  1201. await feed._maybeInsertSearchShortcuts(fakeNewTabUtils.pinnedLinks.links);
  1202. assert.ok(fakeNewTabUtils.pinnedLinks.links.find(s => s && s.url === "https://google.com"));
  1203. assert.notOk(fakeNewTabUtils.pinnedLinks.links.find(s => s && s.url === "https://amazon.com"));
  1204. fakeNewTabUtils.pinnedLinks.links.fill(null);
  1205. feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = "google";
  1206. await feed._maybeInsertSearchShortcuts(fakeNewTabUtils.pinnedLinks.links);
  1207. assert.notOk(fakeNewTabUtils.pinnedLinks.links.find(s => s && s.url === "https://google.com"));
  1208. assert.ok(fakeNewTabUtils.pinnedLinks.links.find(s => s && s.url === "https://amazon.com"));
  1209. });
  1210. it("should record the insertion of a search shortcut", async () => {
  1211. feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = "";
  1212. // Fill up one slot, so there's only one left - to be filled by Google
  1213. fakeNewTabUtils.pinnedLinks.links[3] = {url: ""};
  1214. await feed._maybeInsertSearchShortcuts(fakeNewTabUtils.pinnedLinks.links);
  1215. assert.calledWithExactly(feed.store.dispatch, {
  1216. data: {name: SEARCH_SHORTCUTS_HAVE_PINNED_PREF, value: "google"},
  1217. meta: {from: "ActivityStream:Content", to: "ActivityStream:Main"},
  1218. type: "SET_PREF",
  1219. });
  1220. });
  1221. });
  1222. });
  1223. describe("updatePinnedSearchShortcuts", () => {
  1224. it("should unpin a shortcut in deletedShortcuts", () => {
  1225. const deletedShortcuts = [{url: "https://google.com", searchVendor: "google", label: "google", searchTopSite: true}];
  1226. const addedShortcuts = [];
  1227. fakeNewTabUtils.pinnedLinks.links = [null, null, {url: "https://amazon.com", searchVendor: "amazon", label: "amazon", searchTopSite: true}];
  1228. feed.updatePinnedSearchShortcuts({addedShortcuts, deletedShortcuts});
  1229. assert.notCalled(fakeNewTabUtils.pinnedLinks.pin);
  1230. assert.calledOnce(fakeNewTabUtils.pinnedLinks.unpin);
  1231. assert.calledWith(fakeNewTabUtils.pinnedLinks.unpin, {url: "https://google.com"});
  1232. });
  1233. it("should pin a shortcut in addedShortcuts", () => {
  1234. const addedShortcuts = [{url: "https://google.com", searchVendor: "google", label: "google", searchTopSite: true}];
  1235. const deletedShortcuts = [];
  1236. fakeNewTabUtils.pinnedLinks.links = [null, null, {url: "https://amazon.com", searchVendor: "amazon", label: "amazon", searchTopSite: true}];
  1237. feed.updatePinnedSearchShortcuts({addedShortcuts, deletedShortcuts});
  1238. assert.notCalled(fakeNewTabUtils.pinnedLinks.unpin);
  1239. assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
  1240. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, {label: "google", searchTopSite: true, searchVendor: "google", url: "https://google.com"}, 0);
  1241. });
  1242. it("should pin and unpin in the same action", () => {
  1243. const addedShortcuts = [
  1244. {url: "https://google.com", searchVendor: "google", label: "google", searchTopSite: true},
  1245. {url: "https://ebay.com", searchVendor: "ebay", label: "ebay", searchTopSite: true},
  1246. ];
  1247. const deletedShortcuts = [{url: "https://amazon.com", searchVendor: "amazon", label: "amazon", searchTopSite: true}];
  1248. fakeNewTabUtils.pinnedLinks.links = [{url: "https://foo.com"}, {url: "https://amazon.com", searchVendor: "amazon", label: "amazon", searchTopSite: true}];
  1249. feed.updatePinnedSearchShortcuts({addedShortcuts, deletedShortcuts});
  1250. assert.calledOnce(fakeNewTabUtils.pinnedLinks.unpin);
  1251. assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin);
  1252. });
  1253. it("should pin a shortcut in addedShortcuts even if pinnedLinks is full", () => {
  1254. const addedShortcuts = [{url: "https://google.com", searchVendor: "google", label: "google", searchTopSite: true}];
  1255. const deletedShortcuts = [];
  1256. fakeNewTabUtils.pinnedLinks.links = FAKE_LINKS;
  1257. feed.updatePinnedSearchShortcuts({addedShortcuts, deletedShortcuts});
  1258. assert.notCalled(fakeNewTabUtils.pinnedLinks.unpin);
  1259. assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, {label: "google", searchTopSite: true, url: "https://google.com"}, 0);
  1260. });
  1261. });
  1262. });