LinkMenu.test.jsx (18737B)
1 /* eslint-disable no-shadow */ 2 import { ContextMenu } from "content-src/components/ContextMenu/ContextMenu"; 3 import { _LinkMenu as LinkMenu } from "content-src/components/LinkMenu/LinkMenu"; 4 import React from "react"; 5 import { shallow } from "enzyme"; 6 7 describe("<LinkMenu>", () => { 8 let wrapper; 9 beforeEach(() => { 10 wrapper = shallow( 11 <LinkMenu 12 site={{ url: "" }} 13 options={["CheckPinTopSite", "CheckBookmark", "OpenInNewWindow"]} 14 dispatch={() => {}} 15 /> 16 ); 17 }); 18 it("should render a ContextMenu element", () => { 19 assert.ok(wrapper.find(ContextMenu).exists()); 20 }); 21 it("should pass onUpdate, and options to ContextMenu", () => { 22 assert.ok(wrapper.find(ContextMenu).exists()); 23 const contextMenuProps = wrapper.find(ContextMenu).props(); 24 ["onUpdate", "options"].forEach(prop => 25 assert.property(contextMenuProps, prop) 26 ); 27 }); 28 it("should give ContextMenu the correct tabbable options length for a11y", () => { 29 const { options } = wrapper.find(ContextMenu).props(); 30 const [firstItem] = options; 31 const lastItem = options[options.length - 1]; 32 33 // first item should have {first: true} 34 assert.isTrue(firstItem.first); 35 assert.ok(!firstItem.last); 36 37 // last item should have {last: true} 38 assert.isTrue(lastItem.last); 39 assert.ok(!lastItem.first); 40 41 // middle items should have neither 42 for (let i = 1; i < options.length - 1; i++) { 43 assert.ok(!options[i].first && !options[i].last); 44 } 45 }); 46 it("should show the correct options for default sites", () => { 47 wrapper = shallow( 48 <LinkMenu 49 site={{ url: "", isDefault: true }} 50 options={["CheckBookmark"]} 51 source={"TOP_SITES"} 52 isPrivateBrowsingEnabled={true} 53 dispatch={() => {}} 54 /> 55 ); 56 const { options } = wrapper.find(ContextMenu).props(); 57 let i = 0; 58 assert.propertyVal(options[i++], "id", "newtab-menu-pin"); 59 assert.propertyVal(options[i++], "id", "newtab-menu-edit-topsites"); 60 assert.propertyVal(options[i++], "type", "separator"); 61 assert.propertyVal(options[i++], "id", "newtab-menu-open-new-window"); 62 assert.propertyVal( 63 options[i++], 64 "id", 65 "newtab-menu-open-new-private-window" 66 ); 67 assert.propertyVal(options[i++], "type", "separator"); 68 assert.propertyVal(options[i++], "id", "newtab-menu-dismiss"); 69 assert.propertyVal(options, "length", i); 70 // Double check that delete options are not included for default top sites 71 options 72 .filter(o => o.type !== "separator") 73 .forEach(o => { 74 assert.notInclude(["newtab-menu-delete-history"], o.id); 75 }); 76 }); 77 it("should show Unpin option for a pinned site if CheckPinTopSite in options list", () => { 78 wrapper = shallow( 79 <LinkMenu 80 site={{ url: "", isPinned: true }} 81 source={"TOP_SITES"} 82 options={["CheckPinTopSite"]} 83 dispatch={() => {}} 84 /> 85 ); 86 const { options } = wrapper.find(ContextMenu).props(); 87 assert.isDefined(options.find(o => o.id && o.id === "newtab-menu-unpin")); 88 }); 89 it("should show Pin option for an unpinned site if CheckPinTopSite in options list", () => { 90 wrapper = shallow( 91 <LinkMenu 92 site={{ url: "", isPinned: false }} 93 source={"TOP_SITES"} 94 options={["CheckPinTopSite"]} 95 dispatch={() => {}} 96 /> 97 ); 98 const { options } = wrapper.find(ContextMenu).props(); 99 assert.isDefined(options.find(o => o.id && o.id === "newtab-menu-pin")); 100 }); 101 it("should show Unbookmark option for a bookmarked site if CheckBookmark in options list", () => { 102 wrapper = shallow( 103 <LinkMenu 104 site={{ url: "", bookmarkGuid: 1234 }} 105 source={"TOP_SITES"} 106 options={["CheckBookmark"]} 107 dispatch={() => {}} 108 /> 109 ); 110 const { options } = wrapper.find(ContextMenu).props(); 111 assert.isDefined( 112 options.find(o => o.id && o.id === "newtab-menu-remove-bookmark") 113 ); 114 }); 115 it("should show Bookmark option for an unbookmarked site if CheckBookmark in options list", () => { 116 wrapper = shallow( 117 <LinkMenu 118 site={{ url: "", bookmarkGuid: 0 }} 119 source={"TOP_SITES"} 120 options={["CheckBookmark"]} 121 dispatch={() => {}} 122 /> 123 ); 124 const { options } = wrapper.find(ContextMenu).props(); 125 assert.isDefined( 126 options.find(o => o.id && o.id === "newtab-menu-bookmark") 127 ); 128 }); 129 it("should show Open File option for a downloaded item", () => { 130 wrapper = shallow( 131 <LinkMenu 132 site={{ url: "", type: "download", path: "foo" }} 133 source={"HIGHLIGHTS"} 134 options={["OpenFile"]} 135 dispatch={() => {}} 136 /> 137 ); 138 const { options } = wrapper.find(ContextMenu).props(); 139 assert.isDefined( 140 options.find(o => o.id && o.id === "newtab-menu-open-file") 141 ); 142 }); 143 it("should show Show File option for a downloaded item on a default platform", () => { 144 wrapper = shallow( 145 <LinkMenu 146 site={{ url: "", type: "download", path: "foo" }} 147 source={"HIGHLIGHTS"} 148 options={["ShowFile"]} 149 platform={"default"} 150 dispatch={() => {}} 151 /> 152 ); 153 const { options } = wrapper.find(ContextMenu).props(); 154 assert.isDefined( 155 options.find(o => o.id && o.id === "newtab-menu-show-file") 156 ); 157 }); 158 it("should show Copy Downlad Link option for a downloaded item when CopyDownloadLink", () => { 159 wrapper = shallow( 160 <LinkMenu 161 site={{ url: "", type: "download" }} 162 source={"HIGHLIGHTS"} 163 options={["CopyDownloadLink"]} 164 dispatch={() => {}} 165 /> 166 ); 167 const { options } = wrapper.find(ContextMenu).props(); 168 assert.isDefined( 169 options.find(o => o.id && o.id === "newtab-menu-copy-download-link") 170 ); 171 }); 172 it("should show Go To Download Page option for a downloaded item when GoToDownloadPage", () => { 173 wrapper = shallow( 174 <LinkMenu 175 site={{ url: "", type: "download", referrer: "foo" }} 176 source={"HIGHLIGHTS"} 177 options={["GoToDownloadPage"]} 178 dispatch={() => {}} 179 /> 180 ); 181 const { options } = wrapper.find(ContextMenu).props(); 182 assert.isDefined( 183 options.find(o => o.id && o.id === "newtab-menu-go-to-download-page") 184 ); 185 assert.isFalse(options[0].disabled); 186 }); 187 it("should show Go To Download Page option as disabled for a downloaded item when GoToDownloadPage if no referrer exists", () => { 188 wrapper = shallow( 189 <LinkMenu 190 site={{ url: "", type: "download", referrer: null }} 191 source={"HIGHLIGHTS"} 192 options={["GoToDownloadPage"]} 193 dispatch={() => {}} 194 /> 195 ); 196 const { options } = wrapper.find(ContextMenu).props(); 197 assert.isDefined( 198 options.find(o => o.id && o.id === "newtab-menu-go-to-download-page") 199 ); 200 assert.isTrue(options[0].disabled); 201 }); 202 it("should show Remove Download Link option for a downloaded item when RemoveDownload", () => { 203 wrapper = shallow( 204 <LinkMenu 205 site={{ url: "", type: "download" }} 206 source={"HIGHLIGHTS"} 207 options={["RemoveDownload"]} 208 dispatch={() => {}} 209 /> 210 ); 211 const { options } = wrapper.find(ContextMenu).props(); 212 assert.isDefined( 213 options.find(o => o.id && o.id === "newtab-menu-remove-download") 214 ); 215 }); 216 it("should show Edit option", () => { 217 const props = { url: "foo", label: "label" }; 218 const index = 5; 219 wrapper = shallow( 220 <LinkMenu 221 site={props} 222 index={5} 223 source={"TOP_SITES"} 224 options={["EditTopSite"]} 225 dispatch={() => {}} 226 /> 227 ); 228 const { options } = wrapper.find(ContextMenu).props(); 229 const option = options.find( 230 o => o.id && o.id === "newtab-menu-edit-topsites" 231 ); 232 assert.isDefined(option); 233 assert.equal(option.action.data.index, index); 234 }); 235 describe(".onClick", () => { 236 const FAKE_EVENT = {}; 237 const FAKE_INDEX = 3; 238 const FAKE_SOURCE = "TOP_SITES"; 239 const FAKE_SITE = { 240 bookmarkGuid: 1234, 241 hostname: "foo", 242 path: "foo", 243 pocket_id: "1234", 244 referrer: "https://foo.com/ref", 245 title: "bar", 246 type: "bookmark", 247 typedBonus: true, 248 url: "https://foo.com", 249 sponsored_tile_id: 12345, 250 card_type: "organic", 251 }; 252 const dispatch = sinon.stub(); 253 const propOptions = [ 254 "ShowFile", 255 "CopyDownloadLink", 256 "GoToDownloadPage", 257 "RemoveDownload", 258 "Separator", 259 "ShowPrivacyInfo", 260 "RemoveBookmark", 261 "AddBookmark", 262 "OpenInNewWindow", 263 "OpenInPrivateWindow", 264 "BlockUrl", 265 "DeleteUrl", 266 "PinTopSite", 267 "UnpinTopSite", 268 "WebExtDismiss", 269 ]; 270 const expectedActionData = { 271 "newtab-menu-remove-bookmark": FAKE_SITE.bookmarkGuid, 272 "newtab-menu-bookmark": { 273 url: FAKE_SITE.url, 274 title: FAKE_SITE.title, 275 type: FAKE_SITE.type, 276 }, 277 "newtab-menu-open-new-window": { 278 card_type: FAKE_SITE.card_type, 279 referrer: FAKE_SITE.referrer, 280 typedBonus: FAKE_SITE.typedBonus, 281 url: FAKE_SITE.url, 282 event_source: "CONTEXT_MENU", 283 topic: undefined, 284 firstVisibleTimestamp: undefined, 285 tile_id: undefined, 286 recommendation_id: undefined, 287 scheduled_corpus_item_id: undefined, 288 corpus_item_id: undefined, 289 received_rank: undefined, 290 recommended_at: undefined, 291 format: undefined, 292 is_pocket_card: false, 293 is_sponsored: true, 294 }, 295 "newtab-menu-open-new-private-window": { 296 url: FAKE_SITE.url, 297 referrer: FAKE_SITE.referrer, 298 event_source: "CONTEXT_MENU", 299 }, 300 "newtab-menu-dismiss": [ 301 { 302 url: FAKE_SITE.url, 303 pocket_id: FAKE_SITE.pocket_id, 304 tile_id: 12345, 305 recommendation_id: undefined, 306 scheduled_corpus_item_id: undefined, 307 corpus_item_id: undefined, 308 recommended_at: undefined, 309 received_rank: undefined, 310 isSponsoredTopSite: undefined, 311 type: "bookmark", 312 card_type: FAKE_SITE.card_type, 313 position: 3, 314 is_pocket_card: false, 315 }, 316 ], 317 menu_action_webext_dismiss: { 318 source: "TOP_SITES", 319 url: FAKE_SITE.url, 320 action_position: 3, 321 }, 322 "newtab-menu-delete-history": { 323 url: FAKE_SITE.url, 324 pocket_id: FAKE_SITE.pocket_id, 325 forceBlock: FAKE_SITE.bookmarkGuid, 326 }, 327 "newtab-menu-pin": { site: FAKE_SITE, index: FAKE_INDEX }, 328 "newtab-menu-unpin": { site: { url: FAKE_SITE.url } }, 329 "newtab-menu-show-file": { url: FAKE_SITE.url }, 330 "newtab-menu-copy-download-link": { url: FAKE_SITE.url }, 331 "newtab-menu-go-to-download-page": { url: FAKE_SITE.referrer }, 332 "newtab-menu-remove-download": { url: FAKE_SITE.url }, 333 }; 334 const { options } = shallow( 335 <LinkMenu 336 site={FAKE_SITE} 337 siteInfo={{ value: { card_type: FAKE_SITE.type } }} 338 dispatch={dispatch} 339 index={FAKE_INDEX} 340 isPrivateBrowsingEnabled={true} 341 platform={"default"} 342 options={propOptions} 343 source={FAKE_SOURCE} 344 shouldSendImpressionStats={true} 345 /> 346 ) 347 .find(ContextMenu) 348 .props(); 349 afterEach(() => dispatch.reset()); 350 options 351 .filter(o => o.type !== "separator") 352 .forEach(option => { 353 it(`should fire a ${option.action.type} action for ${option.id} with the expected data`, () => { 354 option.onClick(FAKE_EVENT); 355 356 if (option.impression && option.userEvent) { 357 assert.calledThrice(dispatch); 358 } else if (option.impression || option.userEvent) { 359 assert.calledTwice(dispatch); 360 } else { 361 assert.calledOnce(dispatch); 362 } 363 364 // option.action is dispatched 365 assert.ok(dispatch.firstCall.calledWith(option.action)); 366 367 // option.action has correct data 368 // (delete is a special case as it dispatches a nested DIALOG_OPEN-type action) 369 // in the case of this FAKE_SITE, we send a bookmarkGuid therefore we also want 370 // to block this if we delete it 371 if (option.id === "newtab-menu-delete-history") { 372 assert.deepEqual( 373 option.action.data.onConfirm[0].data, 374 expectedActionData[option.id] 375 ); 376 // Test UserEvent send correct meta about item deleted 377 assert.propertyVal( 378 option.action.data.onConfirm[1].data, 379 "action_position", 380 FAKE_INDEX 381 ); 382 assert.propertyVal( 383 option.action.data.onConfirm[1].data, 384 "source", 385 FAKE_SOURCE 386 ); 387 } else { 388 assert.deepEqual(option.action.data, expectedActionData[option.id]); 389 } 390 }); 391 it(`should fire a UserEvent action for ${option.id} if configured`, () => { 392 if (option.userEvent) { 393 option.onClick(FAKE_EVENT); 394 const [action] = dispatch.secondCall.args; 395 assert.isUserEventAction(action); 396 assert.propertyVal(action.data, "source", FAKE_SOURCE); 397 assert.propertyVal(action.data, "action_position", FAKE_INDEX); 398 assert.propertyVal(action.data.value, "card_type", FAKE_SITE.type); 399 } 400 }); 401 it(`should send impression stats for ${option.id}`, () => { 402 if (option.impression) { 403 option.onClick(FAKE_EVENT); 404 const [action] = dispatch.thirdCall.args; 405 assert.deepEqual(action, option.impression); 406 } 407 }); 408 }); 409 it(`should not send impression stats if not configured`, () => { 410 const fakeOptions = shallow( 411 <LinkMenu 412 site={FAKE_SITE} 413 dispatch={dispatch} 414 index={FAKE_INDEX} 415 options={propOptions} 416 source={FAKE_SOURCE} 417 shouldSendImpressionStats={false} 418 /> 419 ) 420 .find(ContextMenu) 421 .props().options; 422 423 fakeOptions 424 .filter(o => o.type !== "separator") 425 .forEach(option => { 426 if (option.impression) { 427 option.onClick(FAKE_EVENT); 428 assert.calledTwice(dispatch); 429 assert.notEqual(dispatch.firstCall.args[0], option.impression); 430 assert.notEqual(dispatch.secondCall.args[0], option.impression); 431 dispatch.reset(); 432 } 433 }); 434 }); 435 it(`should pin a SPOC with all of the site details sent`, () => { 436 const pinSpocTopSite = "PinTopSite"; 437 const { options: spocOptions } = shallow( 438 <LinkMenu 439 site={FAKE_SITE} 440 siteInfo={{ value: { card_type: FAKE_SITE.type } }} 441 dispatch={dispatch} 442 index={FAKE_INDEX} 443 isPrivateBrowsingEnabled={true} 444 platform={"default"} 445 options={[pinSpocTopSite]} 446 source={FAKE_SOURCE} 447 shouldSendImpressionStats={true} 448 /> 449 ) 450 .find(ContextMenu) 451 .props(); 452 453 const [pinSpocOption] = spocOptions; 454 pinSpocOption.onClick(FAKE_EVENT); 455 456 if (pinSpocOption.impression && pinSpocOption.userEvent) { 457 assert.calledThrice(dispatch); 458 } else if (pinSpocOption.impression || pinSpocOption.userEvent) { 459 assert.calledTwice(dispatch); 460 } else { 461 assert.calledOnce(dispatch); 462 } 463 464 // option.action is dispatched 465 assert.ok(dispatch.firstCall.calledWith(pinSpocOption.action)); 466 467 assert.deepEqual(pinSpocOption.action.data, { 468 site: FAKE_SITE, 469 index: FAKE_INDEX, 470 }); 471 }); 472 it(`should create a proper BLOCK_URL action for a sponsored tile`, () => { 473 const site = { 474 hostname: "foo", 475 path: "foo", 476 referrer: "https://foo.com/ref", 477 title: "bar", 478 type: "bookmark", 479 typedBonus: true, 480 url: "https://foo.com", 481 sponsored_position: 1, 482 }; 483 const { options: blockOptions } = shallow( 484 <LinkMenu 485 site={site} 486 siteInfo={{ value: { card_type: site.type } }} 487 dispatch={dispatch} 488 index={FAKE_INDEX} 489 isPrivateBrowsingEnabled={true} 490 platform={"default"} 491 options={["BlockUrl"]} 492 source={FAKE_SOURCE} 493 shouldSendImpressionStats={true} 494 /> 495 ) 496 .find(ContextMenu) 497 .props(); 498 const [blockUrlOption] = blockOptions; 499 500 blockUrlOption.onClick(FAKE_EVENT); 501 502 assert.calledThrice(dispatch); 503 assert.ok(dispatch.firstCall.calledWith(blockUrlOption.action)); 504 const expected = { 505 url: site.url, 506 pocket_id: undefined, 507 tile_id: undefined, 508 recommendation_id: undefined, 509 scheduled_corpus_item_id: undefined, 510 corpus_item_id: undefined, 511 recommended_at: undefined, 512 received_rank: undefined, 513 advertiser_name: site.hostname, 514 isSponsoredTopSite: 1, 515 type: "bookmark", 516 card_type: undefined, 517 position: 3, 518 is_pocket_card: false, 519 }; 520 assert.deepEqual(blockUrlOption.action.data[0], expected); 521 }); 522 it(`should create a proper BLOCK_URL action for a pocket item`, () => { 523 const site = { 524 hostname: "foo", 525 path: "foo", 526 referrer: "https://foo.com/ref", 527 title: "bar", 528 type: "CardGrid", 529 typedBonus: true, 530 url: "https://foo.com", 531 }; 532 const { options: blockOptions } = shallow( 533 <LinkMenu 534 site={site} 535 siteInfo={{ value: { card_type: site.type } }} 536 dispatch={dispatch} 537 index={FAKE_INDEX} 538 isPrivateBrowsingEnabled={true} 539 platform={"default"} 540 options={["BlockUrl"]} 541 source={FAKE_SOURCE} 542 shouldSendImpressionStats={true} 543 /> 544 ) 545 .find(ContextMenu) 546 .props(); 547 const [blockUrlOption] = blockOptions; 548 549 blockUrlOption.onClick(FAKE_EVENT); 550 551 assert.calledThrice(dispatch); 552 assert.ok(dispatch.firstCall.calledWith(blockUrlOption.action)); 553 const expected = { 554 url: site.url, 555 pocket_id: undefined, 556 tile_id: undefined, 557 recommendation_id: undefined, 558 scheduled_corpus_item_id: undefined, 559 corpus_item_id: undefined, 560 recommended_at: undefined, 561 received_rank: undefined, 562 isSponsoredTopSite: undefined, 563 type: "CardGrid", 564 card_type: undefined, 565 position: 3, 566 is_pocket_card: true, 567 }; 568 assert.deepEqual(blockUrlOption.action.data[0], expected); 569 }); 570 }); 571 });