TopSites.test.jsx (60494B)
1 import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; 2 import { GlobalOverrider } from "test/unit/utils"; 3 import { MIN_RICH_FAVICON_SIZE } from "content-src/components/TopSites/TopSitesConstants"; 4 import { 5 TOP_SITES_DEFAULT_ROWS, 6 TOP_SITES_MAX_SITES_PER_ROW, 7 } from "common/Reducers.sys.mjs"; 8 import { 9 TopSite, 10 TopSiteLink, 11 _TopSiteList as TopSiteList, 12 TopSitePlaceholder, 13 TopSiteAddButton, 14 } from "content-src/components/TopSites/TopSite"; 15 import { 16 INTERSECTION_RATIO, 17 TopSiteImpressionWrapper, 18 } from "content-src/components/TopSites/TopSiteImpressionWrapper"; 19 import { A11yLinkButton } from "content-src/components/A11yLinkButton/A11yLinkButton"; 20 import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu"; 21 import React from "react"; 22 import { mount, shallow } from "enzyme"; 23 import { TopSiteForm } from "content-src/components/TopSites/TopSiteForm"; 24 import { TopSiteFormInput } from "content-src/components/TopSites/TopSiteFormInput"; 25 import { _TopSites as TopSites } from "content-src/components/TopSites/TopSites"; 26 import { ContextMenuButton } from "content-src/components/ContextMenu/ContextMenuButton"; 27 28 const perfSvc = { 29 mark() {}, 30 getMostRecentAbsMarkStartByName() {}, 31 }; 32 33 const DEFAULT_PROPS = { 34 Prefs: { values: { featureConfig: {} } }, 35 TopSites: { initialized: true, rows: [] }, 36 App: { 37 isForStartupCache: false, 38 }, 39 TopSitesRows: TOP_SITES_DEFAULT_ROWS, 40 topSiteIconType: () => "no_image", 41 dispatch() {}, 42 perfSvc, 43 }; 44 45 const DEFAULT_BLOB_URL = "blob://test"; 46 47 describe("<TopSites>", () => { 48 let sandbox; 49 50 beforeEach(() => { 51 sandbox = sinon.createSandbox(); 52 }); 53 54 afterEach(() => { 55 sandbox.restore(); 56 }); 57 58 it("should render a TopSites element", () => { 59 const wrapper = shallow(<TopSites {...DEFAULT_PROPS} />); 60 assert.ok(wrapper.exists()); 61 }); 62 describe("#_dispatchTopSitesStats", () => { 63 let globals; 64 let wrapper; 65 let dispatchStatsSpy; 66 67 beforeEach(() => { 68 globals = new GlobalOverrider(); 69 sandbox.stub(DEFAULT_PROPS, "dispatch"); 70 wrapper = shallow(<TopSites {...DEFAULT_PROPS} />, { 71 disableLifecycleMethods: true, 72 }); 73 dispatchStatsSpy = sandbox.spy( 74 wrapper.instance(), 75 "_dispatchTopSitesStats" 76 ); 77 }); 78 afterEach(() => { 79 globals.restore(); 80 sandbox.restore(); 81 }); 82 it("should call _dispatchTopSitesStats on componentDidMount", () => { 83 wrapper.instance().componentDidMount(); 84 85 assert.calledOnce(dispatchStatsSpy); 86 }); 87 it("should call _dispatchTopSitesStats on componentDidUpdate", () => { 88 wrapper.instance().componentDidUpdate(); 89 90 assert.calledOnce(dispatchStatsSpy); 91 }); 92 it("should dispatch SAVE_SESSION_PERF_DATA", () => { 93 wrapper.instance()._dispatchTopSitesStats(); 94 95 assert.calledOnce(DEFAULT_PROPS.dispatch); 96 assert.calledWithExactly( 97 DEFAULT_PROPS.dispatch, 98 ac.AlsoToMain({ 99 type: at.SAVE_SESSION_PERF_DATA, 100 data: { 101 topsites_icon_stats: { 102 custom_screenshot: 0, 103 screenshot: 0, 104 tippytop: 0, 105 rich_icon: 0, 106 no_image: 0, 107 }, 108 topsites_pinned: 0, 109 topsites_search_shortcuts: 0, 110 }, 111 }) 112 ); 113 }); 114 it("should correctly count TopSite images - just screenshot", () => { 115 const rows = [{ screenshot: true }]; 116 sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows); 117 wrapper.instance()._dispatchTopSitesStats(); 118 119 assert.calledOnce(DEFAULT_PROPS.dispatch); 120 assert.calledWithExactly( 121 DEFAULT_PROPS.dispatch, 122 ac.AlsoToMain({ 123 type: at.SAVE_SESSION_PERF_DATA, 124 data: { 125 topsites_icon_stats: { 126 custom_screenshot: 0, 127 screenshot: 1, 128 tippytop: 0, 129 rich_icon: 0, 130 no_image: 0, 131 }, 132 topsites_pinned: 0, 133 topsites_search_shortcuts: 0, 134 }, 135 }) 136 ); 137 }); 138 it("should correctly count TopSite images - custom_screenshot", () => { 139 const rows = [{ customScreenshotURL: true }]; 140 sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows); 141 wrapper.instance()._dispatchTopSitesStats(); 142 143 assert.calledOnce(DEFAULT_PROPS.dispatch); 144 assert.calledWithExactly( 145 DEFAULT_PROPS.dispatch, 146 ac.AlsoToMain({ 147 type: at.SAVE_SESSION_PERF_DATA, 148 data: { 149 topsites_icon_stats: { 150 custom_screenshot: 1, 151 screenshot: 0, 152 tippytop: 0, 153 rich_icon: 0, 154 no_image: 0, 155 }, 156 topsites_pinned: 0, 157 topsites_search_shortcuts: 0, 158 }, 159 }) 160 ); 161 }); 162 it("should correctly count TopSite images - rich_icon", () => { 163 const rows = [{ faviconSize: MIN_RICH_FAVICON_SIZE }]; 164 sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows); 165 wrapper.instance()._dispatchTopSitesStats(); 166 167 assert.calledOnce(DEFAULT_PROPS.dispatch); 168 assert.calledWithExactly( 169 DEFAULT_PROPS.dispatch, 170 ac.AlsoToMain({ 171 type: at.SAVE_SESSION_PERF_DATA, 172 data: { 173 topsites_icon_stats: { 174 custom_screenshot: 0, 175 screenshot: 0, 176 tippytop: 0, 177 rich_icon: 1, 178 no_image: 0, 179 }, 180 topsites_pinned: 0, 181 topsites_search_shortcuts: 0, 182 }, 183 }) 184 ); 185 }); 186 it("should correctly count TopSite images - tippytop", () => { 187 const rows = [ 188 { tippyTopIcon: "foo" }, 189 { faviconRef: "tippytop" }, 190 { faviconRef: "foobar" }, 191 ]; 192 sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows); 193 wrapper.instance()._dispatchTopSitesStats(); 194 195 assert.calledOnce(DEFAULT_PROPS.dispatch); 196 assert.calledWithExactly( 197 DEFAULT_PROPS.dispatch, 198 ac.AlsoToMain({ 199 type: at.SAVE_SESSION_PERF_DATA, 200 data: { 201 topsites_icon_stats: { 202 custom_screenshot: 0, 203 screenshot: 0, 204 tippytop: 2, 205 rich_icon: 0, 206 no_image: 1, 207 }, 208 topsites_pinned: 0, 209 topsites_search_shortcuts: 0, 210 }, 211 }) 212 ); 213 }); 214 it("should correctly count TopSite images - no image", () => { 215 const rows = [{}]; 216 sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows); 217 wrapper.instance()._dispatchTopSitesStats(); 218 219 assert.calledOnce(DEFAULT_PROPS.dispatch); 220 assert.calledWithExactly( 221 DEFAULT_PROPS.dispatch, 222 ac.AlsoToMain({ 223 type: at.SAVE_SESSION_PERF_DATA, 224 data: { 225 topsites_icon_stats: { 226 custom_screenshot: 0, 227 screenshot: 0, 228 tippytop: 0, 229 rich_icon: 0, 230 no_image: 1, 231 }, 232 topsites_pinned: 0, 233 topsites_search_shortcuts: 0, 234 }, 235 }) 236 ); 237 }); 238 it("should correctly count pinned Top Sites", () => { 239 const rows = [ 240 { isPinned: true }, 241 { isPinned: false }, 242 { isPinned: true }, 243 ]; 244 sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows); 245 wrapper.instance()._dispatchTopSitesStats(); 246 247 assert.calledOnce(DEFAULT_PROPS.dispatch); 248 assert.calledWithExactly( 249 DEFAULT_PROPS.dispatch, 250 ac.AlsoToMain({ 251 type: at.SAVE_SESSION_PERF_DATA, 252 data: { 253 topsites_icon_stats: { 254 custom_screenshot: 0, 255 screenshot: 0, 256 tippytop: 0, 257 rich_icon: 0, 258 no_image: 3, 259 }, 260 topsites_pinned: 2, 261 topsites_search_shortcuts: 0, 262 }, 263 }) 264 ); 265 }); 266 it("should correctly count search shortcut Top Sites", () => { 267 const rows = [{ searchTopSite: true }, { searchTopSite: true }]; 268 sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows); 269 wrapper.instance()._dispatchTopSitesStats(); 270 271 assert.calledOnce(DEFAULT_PROPS.dispatch); 272 assert.calledWithExactly( 273 DEFAULT_PROPS.dispatch, 274 ac.AlsoToMain({ 275 type: at.SAVE_SESSION_PERF_DATA, 276 data: { 277 topsites_icon_stats: { 278 custom_screenshot: 0, 279 screenshot: 0, 280 tippytop: 0, 281 rich_icon: 0, 282 no_image: 2, 283 }, 284 topsites_pinned: 0, 285 topsites_search_shortcuts: 2, 286 }, 287 }) 288 ); 289 }); 290 it("should only count visible top sites on wide layout", () => { 291 globals.set("matchMedia", () => ({ matches: true })); 292 const rows = [ 293 {}, 294 {}, 295 {}, 296 {}, 297 {}, 298 {}, 299 {}, 300 {}, 301 {}, 302 {}, 303 {}, 304 {}, 305 {}, 306 {}, 307 {}, 308 {}, 309 ]; 310 sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows); 311 312 wrapper.instance()._dispatchTopSitesStats(); 313 assert.calledOnce(DEFAULT_PROPS.dispatch); 314 assert.calledWithExactly( 315 DEFAULT_PROPS.dispatch, 316 ac.AlsoToMain({ 317 type: at.SAVE_SESSION_PERF_DATA, 318 data: { 319 topsites_icon_stats: { 320 custom_screenshot: 0, 321 screenshot: 0, 322 tippytop: 0, 323 rich_icon: 0, 324 no_image: 8, 325 }, 326 topsites_pinned: 0, 327 topsites_search_shortcuts: 0, 328 }, 329 }) 330 ); 331 }); 332 it("should only count visible top sites on normal layout", () => { 333 globals.set("matchMedia", () => ({ matches: false })); 334 const rows = [ 335 {}, 336 {}, 337 {}, 338 {}, 339 {}, 340 {}, 341 {}, 342 {}, 343 {}, 344 {}, 345 {}, 346 {}, 347 {}, 348 {}, 349 {}, 350 {}, 351 ]; 352 sandbox.stub(DEFAULT_PROPS.TopSites, "rows").value(rows); 353 wrapper.instance()._dispatchTopSitesStats(); 354 assert.calledOnce(DEFAULT_PROPS.dispatch); 355 assert.calledWithExactly( 356 DEFAULT_PROPS.dispatch, 357 ac.AlsoToMain({ 358 type: at.SAVE_SESSION_PERF_DATA, 359 data: { 360 topsites_icon_stats: { 361 custom_screenshot: 0, 362 screenshot: 0, 363 tippytop: 0, 364 rich_icon: 0, 365 no_image: 6, 366 }, 367 topsites_pinned: 0, 368 topsites_search_shortcuts: 0, 369 }, 370 }) 371 ); 372 }); 373 }); 374 }); 375 376 describe("<TopSiteLink>", () => { 377 let globals; 378 let link; 379 let url; 380 beforeEach(() => { 381 globals = new GlobalOverrider(); 382 url = { 383 createObjectURL: globals.sandbox.stub().returns(DEFAULT_BLOB_URL), 384 revokeObjectURL: globals.sandbox.spy(), 385 }; 386 globals.set("URL", url); 387 link = { url: "https://foo.com", screenshot: "foo.jpg", hostname: "foo" }; 388 }); 389 afterEach(() => globals.restore()); 390 it("should add the right url", () => { 391 link.url = "https://www.foobar.org"; 392 const wrapper = shallow(<TopSiteLink link={link} />); 393 assert.propertyVal( 394 wrapper.find("a").props(), 395 "href", 396 "https://www.foobar.org" 397 ); 398 }); 399 it("should not add the url to the href if it a search shortcut", () => { 400 link.searchTopSite = true; 401 const wrapper = shallow(<TopSiteLink link={link} />); 402 assert.isUndefined(wrapper.find("a").props().href); 403 }); 404 it("should have rtl direction automatically set for text", () => { 405 const wrapper = shallow(<TopSiteLink link={link} />); 406 407 assert.isTrue(!!wrapper.find("[dir='auto']").length); 408 }); 409 it("should render a title", () => { 410 const wrapper = shallow(<TopSiteLink link={link} title="foobar" />); 411 const titleEl = wrapper.find(".title"); 412 413 assert.equal(titleEl.text(), "foobar"); 414 }); 415 it("should have only the title as the text of the link", () => { 416 const wrapper = shallow(<TopSiteLink link={link} title="foobar" />); 417 418 assert.equal(wrapper.find("a").text(), "foobar"); 419 }); 420 it("should render the pin icon for pinned links", () => { 421 link.isPinned = true; 422 link.pinnedIndex = 7; 423 const wrapper = shallow(<TopSiteLink link={link} />); 424 assert.equal(wrapper.find(".icon-pin-small").length, 1); 425 }); 426 it("should not render the pin icon for non pinned links", () => { 427 link.isPinned = false; 428 const wrapper = shallow(<TopSiteLink link={link} />); 429 assert.equal(wrapper.find(".icon-pin-small").length, 0); 430 }); 431 it("should render the first letter of the title as a fallback for missing icons", () => { 432 const wrapper = shallow(<TopSiteLink link={link} title={"foo"} />); 433 assert.equal(wrapper.find(".icon-wrapper").prop("data-fallback"), "f"); 434 }); 435 it("should render the tippy top icon if provided and not a small icon", () => { 436 link.tippyTopIcon = "foo.png"; 437 link.backgroundColor = "#FFFFFF"; 438 const wrapper = shallow(<TopSiteLink link={link} />); 439 assert.lengthOf(wrapper.find(".screenshot"), 0); 440 assert.lengthOf(wrapper.find(".default-icon"), 0); 441 const tippyTop = wrapper.find(".rich-icon"); 442 assert.propertyVal( 443 tippyTop.props().style, 444 "backgroundImage", 445 "url(foo.png)" 446 ); 447 assert.propertyVal(tippyTop.props().style, "backgroundColor", "#FFFFFF"); 448 }); 449 it("should render a rich icon if provided and not a small icon", () => { 450 link.favicon = "foo.png"; 451 link.faviconSize = 196; 452 link.backgroundColor = "#FFFFFF"; 453 const wrapper = shallow(<TopSiteLink link={link} />); 454 assert.lengthOf(wrapper.find(".screenshot"), 0); 455 assert.lengthOf(wrapper.find(".default-icon"), 0); 456 const richIcon = wrapper.find(".rich-icon"); 457 assert.propertyVal( 458 richIcon.props().style, 459 "backgroundImage", 460 "url(foo.png)" 461 ); 462 assert.propertyVal(richIcon.props().style, "backgroundColor", "#FFFFFF"); 463 }); 464 it("should not render a rich icon if it is smaller than 96x96", () => { 465 link.favicon = "foo.png"; 466 link.faviconSize = 48; 467 link.backgroundColor = "#FFFFFF"; 468 const wrapper = shallow(<TopSiteLink link={link} />); 469 assert.lengthOf(wrapper.find(".default-icon"), 1); 470 assert.equal(wrapper.find(".rich-icon").length, 0); 471 }); 472 it("should apply just the default class name to the outer link if props.className is falsey", () => { 473 const wrapper = shallow(<TopSiteLink className={false} />); 474 assert.ok(wrapper.find("li").hasClass("top-site-outer")); 475 }); 476 it("should add props.className to the outer link element", () => { 477 const wrapper = shallow(<TopSiteLink className="foo bar" />); 478 assert.ok(wrapper.find("li").hasClass("top-site-outer foo bar")); 479 }); 480 describe("#_allowDrop", () => { 481 let wrapper; 482 let event; 483 beforeEach(() => { 484 event = { 485 dataTransfer: { 486 types: ["text/topsite-index"], 487 }, 488 }; 489 wrapper = shallow( 490 <TopSiteLink isDraggable={true} onDragEvent={() => {}} /> 491 ); 492 }); 493 it("should be droppable for basic case", () => { 494 const result = wrapper.instance()._allowDrop(event); 495 assert.isTrue(result); 496 }); 497 it("should not be droppable for sponsored_position", () => { 498 wrapper.setProps({ link: { sponsored_position: 1 } }); 499 const result = wrapper.instance()._allowDrop(event); 500 assert.isFalse(result); 501 }); 502 it("should not be droppable for link.type", () => { 503 wrapper.setProps({ link: { type: "SPOC" } }); 504 const result = wrapper.instance()._allowDrop(event); 505 assert.isFalse(result); 506 }); 507 }); 508 describe("#onDragEvent", () => { 509 let simulate; 510 let wrapper; 511 beforeEach(() => { 512 wrapper = shallow( 513 <TopSiteLink isDraggable={true} onDragEvent={() => {}} /> 514 ); 515 simulate = type => { 516 const event = { 517 dataTransfer: { setData() {}, types: { includes() {} } }, 518 preventDefault() { 519 this.prevented = true; 520 }, 521 target: { blur() {} }, 522 type, 523 }; 524 wrapper.simulate(type, event); 525 return event; 526 }; 527 }); 528 it("should allow clicks without dragging", () => { 529 simulate("mousedown"); 530 simulate("mouseup"); 531 532 const event = simulate("click"); 533 534 assert.notOk(event.prevented); 535 }); 536 it("should prevent clicks after dragging", () => { 537 simulate("mousedown"); 538 simulate("dragstart"); 539 simulate("dragenter"); 540 simulate("drop"); 541 simulate("dragend"); 542 simulate("mouseup"); 543 544 const event = simulate("click"); 545 546 assert.ok(event.prevented); 547 }); 548 it("should allow clicks after dragging then clicking", () => { 549 simulate("mousedown"); 550 simulate("dragstart"); 551 simulate("dragenter"); 552 simulate("drop"); 553 simulate("dragend"); 554 simulate("mouseup"); 555 simulate("click"); 556 557 simulate("mousedown"); 558 simulate("mouseup"); 559 560 const event = simulate("click"); 561 562 assert.notOk(event.prevented); 563 }); 564 it("should prevent dragging with sponsored_position from dragstart", () => { 565 const preventDefault = sinon.stub(); 566 // eslint-disable-next-line no-shadow 567 const blur = sinon.stub(); 568 wrapper.setProps({ link: { sponsored_position: 1 } }); 569 wrapper.instance().onDragEvent({ 570 type: "dragstart", 571 preventDefault, 572 target: { blur }, 573 }); 574 assert.calledOnce(preventDefault); 575 assert.calledOnce(blur); 576 assert.isUndefined(wrapper.instance().dragged); 577 }); 578 it("should prevent dragging with link.shim from dragstart", () => { 579 const preventDefault = sinon.stub(); 580 // eslint-disable-next-line no-shadow 581 const blur = sinon.stub(); 582 wrapper.setProps({ link: { type: "SPOC" } }); 583 wrapper.instance().onDragEvent({ 584 type: "dragstart", 585 preventDefault, 586 target: { blur }, 587 }); 588 assert.calledOnce(preventDefault); 589 assert.calledOnce(blur); 590 assert.isUndefined(wrapper.instance().dragged); 591 }); 592 }); 593 594 describe("#generateColor", () => { 595 let colors; 596 beforeEach(() => { 597 colors = "#0090ED,#FF4F5F,#2AC3A2"; 598 }); 599 600 it("should generate a random color but always pick the same color for the same string", async () => { 601 let wrapper = shallow( 602 <TopSiteLink colors={colors} title={"food"} link={link} /> 603 ); 604 605 assert.equal(wrapper.find(".icon-wrapper").prop("data-fallback"), "f"); 606 assert.equal( 607 wrapper.find(".icon-wrapper").prop("style").backgroundColor, 608 colors.split(",")[1] 609 ); 610 assert.ok(true); 611 }); 612 613 it("should generate a different random color", async () => { 614 let wrapper = shallow( 615 <TopSiteLink colors={colors} title={"fam"} link={link} /> 616 ); 617 618 assert.equal( 619 wrapper.find(".icon-wrapper").prop("style").backgroundColor, 620 colors.split(",")[2] 621 ); 622 assert.ok(true); 623 }); 624 625 it("should generate a third random color", async () => { 626 let wrapper = shallow(<TopSiteLink colors={colors} title={"foo"} />); 627 628 assert.equal(wrapper.find(".icon-wrapper").prop("data-fallback"), "f"); 629 assert.equal( 630 wrapper.find(".icon-wrapper").prop("style").backgroundColor, 631 colors.split(",")[0] 632 ); 633 assert.ok(true); 634 }); 635 }); 636 }); 637 638 describe("<TopSite>", () => { 639 let link; 640 beforeEach(() => { 641 link = { url: "https://foo.com", screenshot: "foo.jpg", hostname: "foo" }; 642 }); 643 644 // Build IntersectionObserver class with the arg `entries` for the intersect callback. 645 function buildIntersectionObserver(entries) { 646 return class { 647 constructor(callback) { 648 this.callback = callback; 649 } 650 651 observe() { 652 this.callback(entries); 653 } 654 655 unobserve() {} 656 }; 657 } 658 659 it("should render a TopSite", () => { 660 const wrapper = shallow(<TopSite link={link} />); 661 assert.ok(wrapper.exists()); 662 }); 663 664 it("should render a shortened title based off the url", () => { 665 link.url = "https://www.foobar.org"; 666 link.hostname = "foobar"; 667 link.eTLD = "org"; 668 const wrapper = shallow(<TopSite link={link} />); 669 670 assert.equal(wrapper.find(TopSiteLink).props().title, "foobar"); 671 }); 672 673 it("should parse args for fluent correctly", () => { 674 const title = '"fluent"'; 675 link.hostname = title; 676 677 const wrapper = mount(<TopSite link={link} />); 678 const button = wrapper.find( 679 "button[data-l10n-id='newtab-menu-content-tooltip']" 680 ); 681 assert.equal(button.prop("data-l10n-args"), JSON.stringify({ title })); 682 }); 683 684 it("should have .active class, on top-site-outer if context menu is open", () => { 685 const wrapper = shallow(<TopSite link={link} index={1} activeIndex={1} />); 686 wrapper.setState({ showContextMenu: true }); 687 688 assert.equal(wrapper.find(TopSiteLink).props().className.trim(), "active"); 689 }); 690 it("should not add .active class, on top-site-outer if context menu is closed", () => { 691 const wrapper = shallow(<TopSite link={link} index={1} />); 692 wrapper.setState({ showContextMenu: false, activeTile: 1 }); 693 assert.equal(wrapper.find(TopSiteLink).props().className, ""); 694 }); 695 it("should render a context menu button", () => { 696 const wrapper = shallow(<TopSite link={link} />); 697 assert.equal(wrapper.find(ContextMenuButton).length, 1); 698 }); 699 it("should render a link menu", () => { 700 const wrapper = shallow(<TopSite link={link} />); 701 assert.equal(wrapper.find(LinkMenu).length, 1); 702 }); 703 it("should pass onUpdate, site, options, and index to LinkMenu", () => { 704 const wrapper = shallow(<TopSite link={link} />); 705 const linkMenuProps = wrapper.find(LinkMenu).props(); 706 ["onUpdate", "site", "index", "options"].forEach(prop => 707 assert.property(linkMenuProps, prop) 708 ); 709 }); 710 it("should pass through the correct menu options to LinkMenu", () => { 711 const wrapper = shallow(<TopSite link={link} />); 712 const linkMenuProps = wrapper.find(LinkMenu).props(); 713 assert.deepEqual(linkMenuProps.options, [ 714 "CheckPinTopSite", 715 "EditTopSite", 716 "Separator", 717 "OpenInNewWindow", 718 "OpenInPrivateWindow", 719 "Separator", 720 "BlockUrl", 721 "DeleteUrl", 722 ]); 723 }); 724 it("should record impressions for visible organic Top Sites", () => { 725 const dispatch = sinon.stub(); 726 const wrapper = shallow( 727 <TopSite 728 link={link} 729 index={3} 730 dispatch={dispatch} 731 IntersectionObserver={buildIntersectionObserver([ 732 { 733 isIntersecting: true, 734 intersectionRatio: INTERSECTION_RATIO, 735 }, 736 ])} 737 document={{ 738 visibilityState: "visible", 739 addEventListener: sinon.stub(), 740 removeEventListener: sinon.stub(), 741 }} 742 /> 743 ); 744 const linkWrapper = wrapper.find(TopSiteLink).dive(); 745 assert.ok(linkWrapper.exists()); 746 const impressionWrapper = linkWrapper.find(TopSiteImpressionWrapper).dive(); 747 assert.ok(impressionWrapper.exists()); 748 749 assert.calledOnce(dispatch); 750 751 let [action] = dispatch.firstCall.args; 752 assert.equal(action.type, at.TOP_SITES_ORGANIC_IMPRESSION_STATS); 753 754 assert.propertyVal(action.data, "type", "impression"); 755 assert.propertyVal(action.data, "source", "newtab"); 756 assert.propertyVal(action.data, "position", 3); 757 }); 758 it("should record impressions for visible sponsored Top Sites", () => { 759 const dispatch = sinon.stub(); 760 const wrapper = shallow( 761 <TopSite 762 link={Object.assign({}, link, { 763 sponsored_position: 2, 764 sponsored_tile_id: 12345, 765 sponsored_impression_url: "http://impression.example.com/", 766 })} 767 index={3} 768 dispatch={dispatch} 769 IntersectionObserver={buildIntersectionObserver([ 770 { 771 isIntersecting: true, 772 intersectionRatio: INTERSECTION_RATIO, 773 }, 774 ])} 775 document={{ 776 visibilityState: "visible", 777 addEventListener: sinon.stub(), 778 removeEventListener: sinon.stub(), 779 }} 780 /> 781 ); 782 const linkWrapper = wrapper.find(TopSiteLink).dive(); 783 assert.ok(linkWrapper.exists()); 784 const impressionWrapper = linkWrapper.find(TopSiteImpressionWrapper).dive(); 785 assert.ok(impressionWrapper.exists()); 786 787 assert.calledOnce(dispatch); 788 789 let [action] = dispatch.firstCall.args; 790 assert.equal(action.type, at.TOP_SITES_SPONSORED_IMPRESSION_STATS); 791 792 assert.propertyVal(action.data, "type", "impression"); 793 assert.propertyVal(action.data, "tile_id", 12345); 794 assert.propertyVal(action.data, "source", "newtab"); 795 assert.propertyVal(action.data, "position", 3); 796 assert.propertyVal( 797 action.data, 798 "reporting_url", 799 "http://impression.example.com/" 800 ); 801 assert.propertyVal(action.data, "advertiser", "foo"); 802 }); 803 804 describe("#onLinkClick", () => { 805 it("should call dispatch when the link is clicked", () => { 806 const dispatch = sinon.stub(); 807 const wrapper = shallow( 808 <TopSite link={link} index={3} dispatch={dispatch} /> 809 ); 810 811 wrapper.find(TopSiteLink).simulate("click", { preventDefault() {} }); 812 813 let [action] = dispatch.firstCall.args; 814 assert.isUserEventAction(action); 815 816 assert.propertyVal(action.data, "event", "CLICK"); 817 assert.propertyVal(action.data, "source", "TOP_SITES"); 818 assert.propertyVal(action.data, "action_position", 3); 819 820 [action] = dispatch.secondCall.args; 821 assert.propertyVal(action, "type", at.OPEN_LINK); 822 823 // Organic Top Site click event. 824 [action] = dispatch.thirdCall.args; 825 assert.equal(action.type, at.TOP_SITES_ORGANIC_IMPRESSION_STATS); 826 827 assert.propertyVal(action.data, "type", "click"); 828 assert.propertyVal(action.data, "source", "newtab"); 829 assert.propertyVal(action.data, "position", 3); 830 }); 831 it("should dispatch a UserEventAction with the right data", () => { 832 const dispatch = sinon.stub(); 833 const wrapper = shallow( 834 <TopSite 835 link={Object.assign({}, link, { 836 iconType: "rich_icon", 837 isPinned: true, 838 })} 839 index={3} 840 dispatch={dispatch} 841 /> 842 ); 843 844 wrapper.find(TopSiteLink).simulate("click", { preventDefault() {} }); 845 846 const [action] = dispatch.firstCall.args; 847 assert.isUserEventAction(action); 848 849 assert.propertyVal(action.data, "event", "CLICK"); 850 assert.propertyVal(action.data, "source", "TOP_SITES"); 851 assert.propertyVal(action.data, "action_position", 3); 852 assert.propertyVal(action.data.value, "card_type", "pinned"); 853 assert.propertyVal(action.data.value, "icon_type", "rich_icon"); 854 }); 855 it("should dispatch a UserEventAction with the right data for search top site", () => { 856 const dispatch = sinon.stub(); 857 const siteInfo = { 858 iconType: "tippytop", 859 isPinned: true, 860 searchTopSite: true, 861 hostname: "google", 862 label: "@google", 863 }; 864 const wrapper = shallow( 865 <TopSite 866 link={Object.assign({}, link, siteInfo)} 867 index={3} 868 dispatch={dispatch} 869 /> 870 ); 871 872 wrapper.find(TopSiteLink).simulate("click", { preventDefault() {} }); 873 874 const [action] = dispatch.firstCall.args; 875 assert.isUserEventAction(action); 876 877 assert.propertyVal(action.data, "event", "CLICK"); 878 assert.propertyVal(action.data, "source", "TOP_SITES"); 879 assert.propertyVal(action.data, "action_position", 3); 880 assert.propertyVal(action.data.value, "card_type", "search"); 881 assert.propertyVal(action.data.value, "icon_type", "tippytop"); 882 assert.propertyVal(action.data.value, "search_vendor", "google"); 883 }); 884 it("should dispatch a UserEventAction with the right data for SPOC top site", () => { 885 const dispatch = sinon.stub(); 886 const siteInfo = { 887 id: 1, 888 iconType: "custom_screenshot", 889 type: "SPOC", 890 pos: 1, 891 label: "test advertiser", 892 shim: { click: "shim_click_id" }, 893 }; 894 const wrapper = shallow( 895 <TopSite 896 link={Object.assign({}, link, siteInfo)} 897 index={0} 898 dispatch={dispatch} 899 /> 900 ); 901 902 wrapper.find(TopSiteLink).simulate("click", { preventDefault() {} }); 903 904 let [action] = dispatch.firstCall.args; 905 assert.isUserEventAction(action); 906 907 assert.propertyVal(action.data, "event", "CLICK"); 908 assert.propertyVal(action.data, "source", "TOP_SITES"); 909 assert.propertyVal(action.data, "action_position", 0); 910 assert.propertyVal(action.data.value, "card_type", "spoc"); 911 assert.propertyVal(action.data.value, "icon_type", "custom_screenshot"); 912 913 // Pocket SPOC click event. 914 [action] = dispatch.getCall(2).args; 915 assert.equal(action.type, at.TELEMETRY_IMPRESSION_STATS); 916 917 assert.propertyVal(action.data, "click", 0); 918 assert.propertyVal(action.data, "source", "TOP_SITES"); 919 920 [action] = dispatch.getCall(3).args; 921 assert.equal(action.type, at.DISCOVERY_STREAM_USER_EVENT); 922 923 assert.propertyVal(action.data, "event", "CLICK"); 924 assert.propertyVal(action.data, "action_position", 1); 925 assert.propertyVal(action.data, "source", "TOP_SITES"); 926 assert.propertyVal(action.data.value, "card_type", "spoc"); 927 assert.propertyVal(action.data.value, "tile_id", 1); 928 assert.propertyVal(action.data.value, "shim", "shim_click_id"); 929 930 // Topsite SPOC click event. 931 [action] = dispatch.getCall(4).args; 932 assert.equal(action.type, at.TOP_SITES_SPONSORED_IMPRESSION_STATS); 933 934 assert.propertyVal(action.data, "type", "click"); 935 assert.propertyVal(action.data, "tile_id", 1); 936 assert.propertyVal(action.data, "source", "newtab"); 937 assert.propertyVal(action.data, "position", 1); 938 assert.propertyVal(action.data, "advertiser", "test advertiser"); 939 }); 940 it("should dispatch OPEN_LINK with the right data", () => { 941 const dispatch = sinon.stub(); 942 const wrapper = shallow( 943 <TopSite 944 link={Object.assign({}, link, { typedBonus: true })} 945 index={3} 946 dispatch={dispatch} 947 /> 948 ); 949 950 wrapper.find(TopSiteLink).simulate("click", { preventDefault() {} }); 951 952 const [action] = dispatch.secondCall.args; 953 assert.propertyVal(action, "type", at.OPEN_LINK); 954 assert.propertyVal(action.data, "typedBonus", true); 955 }); 956 }); 957 }); 958 959 describe("<TopSiteForm>", () => { 960 let wrapper; 961 let sandbox; 962 963 function testSetup(props = {}) { 964 sandbox = sinon.createSandbox(); 965 const customProps = Object.assign( 966 {}, 967 { onClose: sandbox.spy(), dispatch: sandbox.spy() }, 968 props 969 ); 970 wrapper = mount(<TopSiteForm {...customProps} />); 971 } 972 973 describe("validateForm", () => { 974 beforeEach(() => testSetup({ site: { url: "http://foo" } })); 975 976 it("should return true for a correct URL", () => { 977 wrapper.setState({ url: "foo" }); 978 979 assert.isTrue(wrapper.instance().validateForm()); 980 }); 981 982 it("should return false for a incorrect URL", () => { 983 wrapper.setState({ url: " " }); 984 985 assert.isNull(wrapper.instance().validateForm()); 986 assert.isTrue(wrapper.state().validationError); 987 }); 988 989 it("should return true for a correct custom screenshot URL", () => { 990 wrapper.setState({ customScreenshotUrl: "foo" }); 991 992 assert.isTrue(wrapper.instance().validateForm()); 993 }); 994 995 it("should return false for a incorrect custom screenshot URL", () => { 996 wrapper.setState({ customScreenshotUrl: " " }); 997 998 assert.isNull(wrapper.instance().validateForm()); 999 }); 1000 1001 it("should return true for an empty custom screenshot URL", () => { 1002 wrapper.setState({ customScreenshotUrl: "" }); 1003 1004 assert.isTrue(wrapper.instance().validateForm()); 1005 }); 1006 1007 it("should return false for file: protocol", () => { 1008 wrapper.setState({ customScreenshotUrl: "file:///C:/Users/foo" }); 1009 1010 assert.isFalse(wrapper.instance().validateForm()); 1011 }); 1012 }); 1013 1014 describe("#previewButton", () => { 1015 beforeEach(() => 1016 testSetup({ 1017 site: { customScreenshotURL: "http://foo.com" }, 1018 previewResponse: null, 1019 }) 1020 ); 1021 1022 it("should render the preview button on invalid urls", () => { 1023 assert.equal(0, wrapper.find(".preview").length); 1024 1025 wrapper.setState({ customScreenshotUrl: " " }); 1026 1027 assert.equal(1, wrapper.find(".preview").length); 1028 }); 1029 1030 it("should render the preview button when input value updated", () => { 1031 assert.equal(0, wrapper.find(".preview").length); 1032 1033 wrapper.setState({ 1034 customScreenshotUrl: "http://baz.com", 1035 screenshotPreview: null, 1036 }); 1037 1038 assert.equal(1, wrapper.find(".preview").length); 1039 }); 1040 }); 1041 1042 describe("preview request", () => { 1043 beforeEach(() => { 1044 testSetup({ 1045 site: { customScreenshotURL: "http://foo.com", url: "http://foo.com" }, 1046 previewResponse: null, 1047 }); 1048 }); 1049 1050 it("shouldn't dispatch a request for invalid urls", () => { 1051 wrapper.setState({ customScreenshotUrl: " ", url: "foo" }); 1052 1053 wrapper.find(".preview").simulate("click"); 1054 1055 assert.notCalled(wrapper.props().dispatch); 1056 }); 1057 1058 it("should dispatch a PREVIEW_REQUEST", () => { 1059 wrapper.setState({ customScreenshotUrl: "screenshot" }); 1060 wrapper.find(".preview").simulate("submit"); 1061 1062 assert.calledTwice(wrapper.props().dispatch); 1063 assert.calledWith( 1064 wrapper.props().dispatch, 1065 ac.AlsoToMain({ 1066 type: at.PREVIEW_REQUEST, 1067 data: { url: "http://screenshot" }, 1068 }) 1069 ); 1070 assert.calledWith( 1071 wrapper.props().dispatch, 1072 ac.UserEvent({ 1073 event: "PREVIEW_REQUEST", 1074 source: "TOP_SITES", 1075 }) 1076 ); 1077 }); 1078 }); 1079 1080 describe("#TopSiteLink", () => { 1081 beforeEach(() => { 1082 testSetup(); 1083 }); 1084 1085 it("should display a TopSiteLink preview", () => { 1086 assert.equal(wrapper.find(TopSiteLink).length, 1); 1087 }); 1088 1089 it("should display an icon for tippyTop sites", () => { 1090 wrapper.setProps({ site: { tippyTopIcon: "bar" } }); 1091 1092 assert.equal( 1093 wrapper.find(".top-site-icon").getDOMNode().style["background-image"], 1094 'url("bar")' 1095 ); 1096 }); 1097 1098 it("should not display a preview screenshot", () => { 1099 wrapper.setProps({ previewResponse: "foo", previewUrl: "foo" }); 1100 1101 assert.lengthOf(wrapper.find(".screenshot"), 0); 1102 }); 1103 1104 it("should not render any icon on error", () => { 1105 wrapper.setProps({ previewResponse: "" }); 1106 1107 assert.equal(wrapper.find(".top-site-icon").length, 0); 1108 }); 1109 1110 it("should render the search icon when searchTopSite is true", () => { 1111 wrapper.setProps({ site: { tippyTopIcon: "bar", searchTopSite: true } }); 1112 1113 assert.equal( 1114 wrapper.find(".rich-icon").getDOMNode().style["background-image"], 1115 'url("bar")' 1116 ); 1117 assert.isTrue(wrapper.find(".search-topsite").exists()); 1118 }); 1119 }); 1120 1121 describe("#addMode", () => { 1122 beforeEach(() => testSetup()); 1123 1124 it("should render the component", () => { 1125 assert.ok(wrapper.find(TopSiteForm).exists()); 1126 }); 1127 it("should have the correct header", () => { 1128 assert.equal( 1129 wrapper.findWhere( 1130 n => 1131 n.length && 1132 n.prop("data-l10n-id") === "newtab-topsites-add-shortcut-header" 1133 ).length, 1134 1 1135 ); 1136 }); 1137 it("should have the correct button text", () => { 1138 assert.equal( 1139 wrapper.findWhere( 1140 n => 1141 n.length && n.prop("data-l10n-id") === "newtab-topsites-save-button" 1142 ).length, 1143 0 1144 ); 1145 assert.equal( 1146 wrapper.findWhere( 1147 n => 1148 n.length && n.prop("data-l10n-id") === "newtab-topsites-add-button" 1149 ).length, 1150 1 1151 ); 1152 }); 1153 it("should not render a preview button", () => { 1154 assert.equal(0, wrapper.find(".custom-image-input-container").length); 1155 }); 1156 it("should call onClose if Cancel button is clicked", () => { 1157 wrapper.find(".cancel").simulate("click"); 1158 assert.calledOnce(wrapper.instance().props.onClose); 1159 }); 1160 it("should set validationError if url is empty", () => { 1161 assert.equal(wrapper.state().validationError, false); 1162 wrapper.find(".done").simulate("submit"); 1163 assert.equal(wrapper.state().validationError, true); 1164 }); 1165 it("should set validationError if url is invalid", () => { 1166 wrapper.setState({ url: "not valid" }); 1167 assert.equal(wrapper.state().validationError, false); 1168 wrapper.find(".done").simulate("submit"); 1169 assert.equal(wrapper.state().validationError, true); 1170 }); 1171 it("should call onClose and dispatch with right args if URL is valid", () => { 1172 wrapper.setState({ url: "valid.com", label: "a label" }); 1173 wrapper.find(".done").simulate("submit"); 1174 assert.calledOnce(wrapper.instance().props.onClose); 1175 assert.calledWith(wrapper.instance().props.dispatch, { 1176 data: { 1177 site: { label: "a label", url: "http://valid.com" }, 1178 index: -1, 1179 }, 1180 meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" }, 1181 type: at.TOP_SITES_PIN, 1182 }); 1183 assert.calledWith(wrapper.instance().props.dispatch, { 1184 data: { 1185 action_position: -1, 1186 source: "TOP_SITES", 1187 event: "TOP_SITES_ADD", 1188 }, 1189 meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" }, 1190 type: at.TELEMETRY_USER_EVENT, 1191 }); 1192 }); 1193 it("should not pass empty string label in dispatch data", () => { 1194 wrapper.setState({ url: "valid.com", label: "" }); 1195 wrapper.find(".done").simulate("submit"); 1196 assert.calledWith(wrapper.instance().props.dispatch, { 1197 data: { site: { url: "http://valid.com" }, index: -1 }, 1198 meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" }, 1199 type: at.TOP_SITES_PIN, 1200 }); 1201 }); 1202 it("should open the custom screenshot input", () => { 1203 assert.isFalse(wrapper.state().showCustomScreenshotForm); 1204 1205 wrapper.find(A11yLinkButton).simulate("click"); 1206 1207 assert.isTrue(wrapper.state().showCustomScreenshotForm); 1208 }); 1209 }); 1210 1211 describe("edit existing Topsite", () => { 1212 beforeEach(() => 1213 testSetup({ 1214 site: { 1215 url: "https://foo.bar", 1216 label: "baz", 1217 customScreenshotURL: "http://foo", 1218 }, 1219 index: 7, 1220 }) 1221 ); 1222 1223 it("should render the component", () => { 1224 assert.ok(wrapper.find(TopSiteForm).exists()); 1225 }); 1226 it("should have the correct header", () => { 1227 assert.equal( 1228 wrapper.findWhere( 1229 n => n.prop("data-l10n-id") === "newtab-topsites-edit-shortcut-header" 1230 ).length, 1231 1 1232 ); 1233 }); 1234 it("should have the correct button text", () => { 1235 assert.equal( 1236 wrapper.findWhere( 1237 n => n.prop("data-l10n-id") === "newtab-topsites-add-button" 1238 ).length, 1239 0 1240 ); 1241 assert.equal( 1242 wrapper.findWhere( 1243 n => n.prop("data-l10n-id") === "newtab-topsites-save-button" 1244 ).length, 1245 1 1246 ); 1247 }); 1248 it("should call onClose if Cancel button is clicked", () => { 1249 wrapper.find(".cancel").simulate("click"); 1250 assert.calledOnce(wrapper.instance().props.onClose); 1251 }); 1252 it("should show error and not call onClose or dispatch if URL is empty", () => { 1253 wrapper.setState({ url: "" }); 1254 assert.equal(wrapper.state().validationError, false); 1255 wrapper.find(".done").simulate("submit"); 1256 assert.equal(wrapper.state().validationError, true); 1257 assert.notCalled(wrapper.instance().props.onClose); 1258 assert.notCalled(wrapper.instance().props.dispatch); 1259 }); 1260 it("should show error and not call onClose or dispatch if URL is invalid", () => { 1261 wrapper.setState({ url: "not valid" }); 1262 assert.equal(wrapper.state().validationError, false); 1263 wrapper.find(".done").simulate("submit"); 1264 assert.equal(wrapper.state().validationError, true); 1265 assert.notCalled(wrapper.instance().props.onClose); 1266 assert.notCalled(wrapper.instance().props.dispatch); 1267 }); 1268 it("should call onClose and dispatch with right args if URL is valid", () => { 1269 wrapper.find(".done").simulate("submit"); 1270 assert.calledOnce(wrapper.instance().props.onClose); 1271 assert.calledTwice(wrapper.instance().props.dispatch); 1272 assert.calledWith(wrapper.instance().props.dispatch, { 1273 data: { 1274 site: { 1275 label: "baz", 1276 url: "https://foo.bar", 1277 customScreenshotURL: "http://foo", 1278 }, 1279 index: 7, 1280 }, 1281 meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" }, 1282 type: at.TOP_SITES_PIN, 1283 }); 1284 assert.calledWith(wrapper.instance().props.dispatch, { 1285 data: { 1286 action_position: 7, 1287 source: "TOP_SITES", 1288 event: "TOP_SITES_EDIT", 1289 hasTitleChanged: false, 1290 hasURLChanged: false, 1291 }, 1292 meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" }, 1293 type: at.TELEMETRY_USER_EVENT, 1294 }); 1295 }); 1296 it("should set customScreenshotURL to null if it was removed", () => { 1297 wrapper.setState({ customScreenshotUrl: "" }); 1298 1299 wrapper.find(".done").simulate("submit"); 1300 1301 assert.calledWith(wrapper.instance().props.dispatch, { 1302 data: { 1303 site: { 1304 label: "baz", 1305 url: "https://foo.bar", 1306 customScreenshotURL: null, 1307 }, 1308 index: 7, 1309 }, 1310 meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" }, 1311 type: at.TOP_SITES_PIN, 1312 }); 1313 }); 1314 it("should call onClose and dispatch with right args if URL is valid (negative index)", () => { 1315 wrapper.setProps({ index: -1 }); 1316 wrapper.find(".done").simulate("submit"); 1317 assert.calledOnce(wrapper.instance().props.onClose); 1318 assert.calledTwice(wrapper.instance().props.dispatch); 1319 assert.calledWith(wrapper.instance().props.dispatch, { 1320 data: { 1321 site: { 1322 label: "baz", 1323 url: "https://foo.bar", 1324 customScreenshotURL: "http://foo", 1325 }, 1326 index: -1, 1327 }, 1328 meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" }, 1329 type: at.TOP_SITES_PIN, 1330 }); 1331 }); 1332 it("should not pass empty string label in dispatch data", () => { 1333 wrapper.setState({ label: "" }); 1334 wrapper.find(".done").simulate("submit"); 1335 assert.calledWith(wrapper.instance().props.dispatch, { 1336 data: { 1337 site: { url: "https://foo.bar", customScreenshotURL: "http://foo" }, 1338 index: 7, 1339 }, 1340 meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" }, 1341 type: at.TOP_SITES_PIN, 1342 }); 1343 }); 1344 it("should render the save button if custom screenshot request finished", () => { 1345 wrapper.setState({ 1346 customScreenshotUrl: "foo", 1347 screenshotPreview: "custom", 1348 }); 1349 assert.equal(0, wrapper.find(".preview").length); 1350 assert.equal(1, wrapper.find(".done").length); 1351 }); 1352 it("should render the save button if custom screenshot url was cleared", () => { 1353 wrapper.setState({ customScreenshotUrl: "" }); 1354 wrapper.setProps({ site: { customScreenshotURL: "foo" } }); 1355 assert.equal(0, wrapper.find(".preview").length); 1356 assert.equal(1, wrapper.find(".done").length); 1357 }); 1358 }); 1359 1360 describe("#previewMode", () => { 1361 beforeEach(() => testSetup({ previewResponse: null })); 1362 1363 it("should transition from save to preview", () => { 1364 wrapper.setProps({ 1365 site: { url: "https://foo.bar", customScreenshotURL: "baz" }, 1366 index: 7, 1367 }); 1368 1369 assert.equal( 1370 wrapper.findWhere( 1371 n => 1372 n.length && n.prop("data-l10n-id") === "newtab-topsites-save-button" 1373 ).length, 1374 1 1375 ); 1376 1377 wrapper.setState({ customScreenshotUrl: "foo" }); 1378 1379 assert.equal( 1380 wrapper.findWhere( 1381 n => 1382 n.length && 1383 n.prop("data-l10n-id") === "newtab-topsites-preview-button" 1384 ).length, 1385 1 1386 ); 1387 }); 1388 1389 it("should transition from add to preview", () => { 1390 assert.equal( 1391 wrapper.findWhere( 1392 n => 1393 n.length && n.prop("data-l10n-id") === "newtab-topsites-add-button" 1394 ).length, 1395 1 1396 ); 1397 1398 wrapper.setState({ customScreenshotUrl: "foo" }); 1399 1400 assert.equal( 1401 wrapper.findWhere( 1402 n => 1403 n.length && 1404 n.prop("data-l10n-id") === "newtab-topsites-preview-button" 1405 ).length, 1406 1 1407 ); 1408 }); 1409 }); 1410 1411 describe("#validateUrl", () => { 1412 it("should properly validate URLs", () => { 1413 testSetup(); 1414 assert.ok(wrapper.instance().validateUrl("mozilla.org")); 1415 assert.ok(wrapper.instance().validateUrl("https://mozilla.org")); 1416 assert.ok(wrapper.instance().validateUrl("http://mozilla.org")); 1417 assert.ok( 1418 wrapper 1419 .instance() 1420 .validateUrl( 1421 "https://mozilla.invisionapp.com/d/main/#/projects/prototypes" 1422 ) 1423 ); 1424 assert.ok(wrapper.instance().validateUrl("httpfoobar")); 1425 assert.ok(wrapper.instance().validateUrl("httpsfoo.bar")); 1426 assert.isNull(wrapper.instance().validateUrl("mozilla org")); 1427 assert.isNull(wrapper.instance().validateUrl("")); 1428 }); 1429 }); 1430 1431 describe("#cleanUrl", () => { 1432 it("should properly prepend http:// to URLs when required", () => { 1433 testSetup(); 1434 assert.equal( 1435 "http://mozilla.org", 1436 wrapper.instance().cleanUrl("mozilla.org") 1437 ); 1438 assert.equal( 1439 "http://https.org", 1440 wrapper.instance().cleanUrl("https.org") 1441 ); 1442 assert.equal("http://httpcom", wrapper.instance().cleanUrl("httpcom")); 1443 assert.equal( 1444 "http://mozilla.org", 1445 wrapper.instance().cleanUrl("http://mozilla.org") 1446 ); 1447 assert.equal( 1448 "https://firefox.com", 1449 wrapper.instance().cleanUrl("https://firefox.com") 1450 ); 1451 }); 1452 }); 1453 }); 1454 1455 describe("<TopSiteList>", () => { 1456 const APP = { isForStartupCache: { App: false } }; 1457 1458 it("should render a TopSiteList element", () => { 1459 const wrapper = shallow(<TopSiteList {...DEFAULT_PROPS} App={APP} />); 1460 assert.ok(wrapper.exists()); 1461 }); 1462 it("should render a TopSite for each link with the right url", () => { 1463 const rows = [{ url: "https://foo.com" }, { url: "https://bar.com" }]; 1464 const wrapper = shallow( 1465 <TopSiteList {...DEFAULT_PROPS} TopSites={{ rows }} App={APP} /> 1466 ); 1467 const links = wrapper.find(TopSite); 1468 assert.lengthOf(links, 2); 1469 rows.forEach((row, i) => 1470 assert.equal(links.get(i).props.link.url, row.url) 1471 ); 1472 }); 1473 it("should slice the TopSite rows to the TopSitesRows pref", () => { 1474 const rows = []; 1475 for ( 1476 let i = 0; 1477 i < TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW + 3; 1478 i++ 1479 ) { 1480 rows.push({ url: `https://foo${i}.com` }); 1481 } 1482 const wrapper = shallow( 1483 <TopSiteList 1484 {...DEFAULT_PROPS} 1485 TopSites={{ rows }} 1486 TopSitesRows={TOP_SITES_DEFAULT_ROWS} 1487 App={APP} 1488 /> 1489 ); 1490 const links = wrapper.find(TopSite); 1491 assert.lengthOf( 1492 links, 1493 TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW 1494 ); 1495 }); 1496 it("should add a add topsite button if there is availible space in the row", () => { 1497 const rows = [{ url: "https://foo.com" }, { url: "https://bar.com" }]; 1498 const availibleRows = 1; 1499 const wrapper = shallow( 1500 <TopSiteList 1501 {...DEFAULT_PROPS} 1502 TopSites={{ rows }} 1503 TopSitesRows={availibleRows} 1504 App={APP} 1505 /> 1506 ); 1507 assert.lengthOf(wrapper.find(TopSite), 2, "topSites"); 1508 assert.lengthOf( 1509 wrapper.find(TopSiteAddButton), 1510 availibleRows >= wrapper.find(TopSite).length ? 0 : 1, 1511 "placeholders" 1512 ); 1513 }); 1514 it("should fill sponsored top sites with placeholders while rendering for startup cache", () => { 1515 const rows = [ 1516 { url: "https://sponsored01.com", sponsored_position: 1 }, 1517 { url: "https://sponsored02.com", sponsored_position: 2 }, 1518 { url: "https://sponsored03.com", type: "SPOC" }, 1519 { url: "https://foo.com" }, 1520 { url: "https://bar.com" }, 1521 ]; 1522 const wrapper = shallow( 1523 <TopSiteList 1524 {...DEFAULT_PROPS} 1525 TopSites={{ rows }} 1526 TopSitesRows={1} 1527 App={{ isForStartupCache: { TopSites: true } }} 1528 /> 1529 ); 1530 assert.lengthOf(wrapper.find(TopSite), 2, "topSites"); 1531 assert.lengthOf(wrapper.find(TopSitePlaceholder), 3, "placeholders"); 1532 }); 1533 it("should update state onDragStart and clear it onDragEnd", () => { 1534 const wrapper = shallow(<TopSiteList {...DEFAULT_PROPS} App={{ APP }} />); 1535 const instance = wrapper.instance(); 1536 const index = 7; 1537 const link = { url: "https://foo.com" }; 1538 const title = "foo"; 1539 instance.onDragEvent({ type: "dragstart" }, index, link, title); 1540 assert.equal(instance.state.draggedIndex, index); 1541 assert.equal(instance.state.draggedSite, link); 1542 assert.equal(instance.state.draggedTitle, title); 1543 instance.onDragEvent({ type: "dragend" }); 1544 assert.deepEqual(instance.state, TopSiteList.DEFAULT_STATE); 1545 }); 1546 it("should clear state when new props arrive after a drop", () => { 1547 const site1 = { url: "https://foo.com" }; 1548 const site2 = { url: "https://bar.com" }; 1549 const rows = [site1, site2]; 1550 const wrapper = shallow( 1551 <TopSiteList {...DEFAULT_PROPS} TopSites={{ rows }} App={APP} /> 1552 ); 1553 const instance = wrapper.instance(); 1554 instance.setState({ 1555 draggedIndex: 1, 1556 draggedSite: site2, 1557 draggedTitle: "bar", 1558 topSitesPreview: [], 1559 }); 1560 wrapper.setProps({ TopSites: { rows: [site2, site1] } }); 1561 assert.deepEqual(instance.state, TopSiteList.DEFAULT_STATE); 1562 }); 1563 it("should dispatch events on drop", () => { 1564 const dispatch = sinon.spy(); 1565 const wrapper = shallow( 1566 <TopSiteList {...DEFAULT_PROPS} dispatch={dispatch} App={APP} /> 1567 ); 1568 const instance = wrapper.instance(); 1569 const index = 7; 1570 const link = { url: "https://foo.com", customScreenshotURL: "foo" }; 1571 const title = "foo"; 1572 instance.onDragEvent({ type: "dragstart" }, index, link, title); 1573 dispatch.resetHistory(); 1574 instance.onDragEvent({ type: "drop" }, 3); 1575 assert.calledTwice(dispatch); 1576 assert.calledWith(dispatch, { 1577 data: { 1578 draggedFromIndex: 7, 1579 index: 3, 1580 site: { 1581 label: "foo", 1582 url: "https://foo.com", 1583 customScreenshotURL: "foo", 1584 }, 1585 }, 1586 meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" }, 1587 type: "TOP_SITES_INSERT", 1588 }); 1589 assert.calledWith(dispatch, { 1590 data: { action_position: 3, event: "DROP", source: "TOP_SITES" }, 1591 meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" }, 1592 type: "TELEMETRY_USER_EVENT", 1593 }); 1594 }); 1595 it("should make a topSitesPreview onDragEnter", () => { 1596 const wrapper = shallow(<TopSiteList {...DEFAULT_PROPS} App={APP} />); 1597 const instance = wrapper.instance(); 1598 const site = { url: "https://foo.com" }; 1599 instance.setState({ 1600 draggedIndex: 4, 1601 draggedSite: site, 1602 draggedTitle: "foo", 1603 }); 1604 const draggedSite = Object.assign({}, site, { 1605 isPinned: true, 1606 isDragged: true, 1607 }); 1608 instance.onDragEvent({ type: "dragenter" }, 2); 1609 assert.ok(instance.state.topSitesPreview); 1610 assert.deepEqual(instance.state.topSitesPreview[2], draggedSite); 1611 }); 1612 it("should _makeTopSitesPreview correctly", () => { 1613 const site1 = { url: "https://foo.com" }; 1614 const site2 = { url: "https://bar.com" }; 1615 const site3 = { url: "https://baz.com" }; 1616 const rows = [site1, site2, site3]; 1617 let wrapper = shallow( 1618 <TopSiteList 1619 {...DEFAULT_PROPS} 1620 TopSites={{ rows }} 1621 TopSitesRows={1} 1622 App={APP} 1623 /> 1624 ); 1625 const addButton = { isAddButton: true }; 1626 let instance = wrapper.instance(); 1627 instance.setState({ 1628 draggedIndex: 0, 1629 draggedSite: site1, 1630 draggedTitle: "foo", 1631 }); 1632 let draggedSite = Object.assign({}, site1, { 1633 isPinned: true, 1634 isDragged: true, 1635 }); 1636 assert.deepEqual(instance._makeTopSitesPreview(1), [ 1637 site2, 1638 draggedSite, 1639 site3, 1640 addButton, 1641 null, 1642 null, 1643 null, 1644 null, 1645 ]); 1646 assert.deepEqual(instance._makeTopSitesPreview(2), [ 1647 site2, 1648 site3, 1649 draggedSite, 1650 addButton, 1651 null, 1652 null, 1653 null, 1654 null, 1655 ]); 1656 assert.deepEqual(instance._makeTopSitesPreview(3), [ 1657 site2, 1658 site3, 1659 addButton, 1660 draggedSite, 1661 null, 1662 null, 1663 null, 1664 null, 1665 ]); 1666 site2.isPinned = true; 1667 assert.deepEqual(instance._makeTopSitesPreview(1), [ 1668 site2, 1669 draggedSite, 1670 site3, 1671 addButton, 1672 null, 1673 null, 1674 null, 1675 null, 1676 ]); 1677 assert.deepEqual(instance._makeTopSitesPreview(2), [ 1678 site3, 1679 site2, 1680 draggedSite, 1681 addButton, 1682 null, 1683 null, 1684 null, 1685 null, 1686 ]); 1687 site3.isPinned = true; 1688 assert.deepEqual(instance._makeTopSitesPreview(1), [ 1689 site2, 1690 draggedSite, 1691 site3, 1692 addButton, 1693 null, 1694 null, 1695 null, 1696 null, 1697 ]); 1698 assert.deepEqual(instance._makeTopSitesPreview(2), [ 1699 site2, 1700 site3, 1701 draggedSite, 1702 addButton, 1703 null, 1704 null, 1705 null, 1706 null, 1707 ]); 1708 site2.isPinned = false; 1709 assert.deepEqual(instance._makeTopSitesPreview(1), [ 1710 site2, 1711 draggedSite, 1712 site3, 1713 addButton, 1714 null, 1715 null, 1716 null, 1717 null, 1718 ]); 1719 assert.deepEqual(instance._makeTopSitesPreview(2), [ 1720 site2, 1721 site3, 1722 draggedSite, 1723 addButton, 1724 null, 1725 null, 1726 null, 1727 null, 1728 ]); 1729 site3.isPinned = false; 1730 instance.setState({ 1731 draggedIndex: 1, 1732 draggedSite: site2, 1733 draggedTitle: "bar", 1734 }); 1735 draggedSite = Object.assign({}, site2, { isPinned: true, isDragged: true }); 1736 assert.deepEqual(instance._makeTopSitesPreview(0), [ 1737 draggedSite, 1738 site1, 1739 site3, 1740 addButton, 1741 null, 1742 null, 1743 null, 1744 null, 1745 ]); 1746 assert.deepEqual(instance._makeTopSitesPreview(2), [ 1747 site1, 1748 site3, 1749 draggedSite, 1750 addButton, 1751 null, 1752 null, 1753 null, 1754 null, 1755 ]); 1756 site2.type = "SPOC"; 1757 instance.setState({ 1758 draggedIndex: 2, 1759 draggedSite: site3, 1760 draggedTitle: "baz", 1761 }); 1762 draggedSite = Object.assign({}, site3, { isPinned: true, isDragged: true }); 1763 assert.deepEqual(instance._makeTopSitesPreview(0), [ 1764 draggedSite, 1765 site2, 1766 site1, 1767 addButton, 1768 null, 1769 null, 1770 null, 1771 null, 1772 ]); 1773 site2.type = ""; 1774 site2.sponsored_position = 2; 1775 instance.setState({ 1776 draggedIndex: 2, 1777 draggedSite: site3, 1778 draggedTitle: "baz", 1779 }); 1780 draggedSite = Object.assign({}, site3, { isPinned: true, isDragged: true }); 1781 assert.deepEqual(instance._makeTopSitesPreview(0), [ 1782 draggedSite, 1783 site2, 1784 site1, 1785 addButton, 1786 null, 1787 null, 1788 null, 1789 null, 1790 ]); 1791 }); 1792 it("should add a className hide-for-narrow to sites after 6/row", () => { 1793 const rows = []; 1794 for (let i = 0; i < TOP_SITES_MAX_SITES_PER_ROW; i++) { 1795 rows.push({ url: `https://foo${i}.com` }); 1796 } 1797 const wrapper = mount( 1798 <TopSiteList 1799 {...DEFAULT_PROPS} 1800 TopSites={{ rows }} 1801 TopSitesRows={1} 1802 App={APP} 1803 /> 1804 ); 1805 assert.lengthOf(wrapper.find("li.hide-for-narrow"), 2); 1806 }); 1807 1808 describe("Keyboard navigation", () => { 1809 let sandbox; 1810 let wrapper; 1811 let instance; 1812 let mockAnchor; 1813 let mockTargetSibling; 1814 1815 beforeEach(() => { 1816 sandbox = sinon.createSandbox(); 1817 const rows = [ 1818 { url: "https://foo.com" }, 1819 { url: "https://bar.com" }, 1820 { url: "https://baz.com" }, 1821 ]; 1822 wrapper = shallow( 1823 <TopSiteList {...DEFAULT_PROPS} TopSites={{ rows }} App={APP} /> 1824 ); 1825 instance = wrapper.instance(); 1826 1827 mockAnchor = { focus: sandbox.spy(), tabIndex: -1 }; 1828 mockTargetSibling = { querySelector: sandbox.stub().returns(mockAnchor) }; 1829 }); 1830 1831 afterEach(() => { 1832 sandbox.restore(); 1833 }); 1834 1835 it("should navigate to next site with ArrowRight", () => { 1836 instance.focusedRef = { nextSibling: mockTargetSibling }; 1837 const mockEvent = { key: "ArrowRight" }; 1838 1839 instance.onKeyDown(mockEvent); 1840 1841 assert.calledOnce(mockTargetSibling.querySelector); 1842 assert.calledWith(mockTargetSibling.querySelector, "a"); 1843 assert.calledOnce(mockAnchor.focus); 1844 assert.equal(mockAnchor.tabIndex, 0); 1845 }); 1846 1847 it("should navigate to previous site with ArrowLeft", () => { 1848 instance.focusedRef = { previousSibling: mockTargetSibling }; 1849 const mockEvent = { key: "ArrowLeft" }; 1850 1851 instance.onKeyDown(mockEvent); 1852 1853 assert.calledOnce(mockTargetSibling.querySelector); 1854 assert.calledWith(mockTargetSibling.querySelector, "a"); 1855 assert.calledOnce(mockAnchor.focus); 1856 assert.equal(mockAnchor.tabIndex, 0); 1857 }); 1858 }); 1859 }); 1860 1861 describe("TopSiteAddButton", () => { 1862 it("should dispatch a TOP_SITES_EDIT action when the addbutton is clicked", () => { 1863 const dispatch = sinon.spy(); 1864 const wrapper = shallow( 1865 <TopSiteAddButton dispatch={dispatch} index={7} isAddButton={true} /> 1866 ); 1867 1868 wrapper.find(".add-button").first().simulate("click"); 1869 1870 assert.calledOnce(dispatch); 1871 assert.calledWithExactly(dispatch, { 1872 type: at.TOP_SITES_EDIT, 1873 data: { index: 7 }, 1874 }); 1875 }); 1876 }); 1877 1878 describe("#TopSiteFormInput", () => { 1879 let wrapper; 1880 let onChangeStub; 1881 1882 describe("no errors", () => { 1883 beforeEach(() => { 1884 onChangeStub = sinon.stub(); 1885 1886 wrapper = mount( 1887 <TopSiteFormInput 1888 titleId="newtab-topsites-title-label" 1889 placeholderId="newtab-topsites-title-input" 1890 errorMessageId="newtab-topsites-url-validation" 1891 onChange={onChangeStub} 1892 value="foo" 1893 /> 1894 ); 1895 }); 1896 1897 it("should render the provided title", () => { 1898 const title = wrapper.find("span"); 1899 assert.propertyVal( 1900 title.props(), 1901 "data-l10n-id", 1902 "newtab-topsites-title-label" 1903 ); 1904 }); 1905 1906 it("should render the provided value", () => { 1907 const input = wrapper.find("input"); 1908 1909 assert.equal(input.getDOMNode().value, "foo"); 1910 }); 1911 1912 it("should render the clear button if cb is provided", () => { 1913 assert.equal(wrapper.find(".icon-clear-input").length, 0); 1914 1915 wrapper.setProps({ onClear: sinon.stub() }); 1916 1917 assert.equal(wrapper.find(".icon-clear-input").length, 1); 1918 }); 1919 1920 it("should show the loading indicator", () => { 1921 assert.equal(wrapper.find(".loading-container").length, 0); 1922 1923 wrapper.setProps({ loading: true }); 1924 1925 assert.equal(wrapper.find(".loading-container").length, 1); 1926 }); 1927 it("should disable the input when loading indicator is present", () => { 1928 assert.isFalse(wrapper.find("input").getDOMNode().disabled); 1929 1930 wrapper.setProps({ loading: true }); 1931 1932 assert.isTrue(wrapper.find("input").getDOMNode().disabled); 1933 }); 1934 }); 1935 1936 describe("with error", () => { 1937 beforeEach(() => { 1938 onChangeStub = sinon.stub(); 1939 1940 wrapper = mount( 1941 <TopSiteFormInput 1942 titleId="newtab-topsites-title-label" 1943 placeholderId="newtab-topsites-title-input" 1944 onChange={onChangeStub} 1945 validationError={true} 1946 errorMessageId="newtab-topsites-url-validation" 1947 value="foo" 1948 /> 1949 ); 1950 }); 1951 1952 it("should render the error message", () => { 1953 assert.equal( 1954 wrapper.findWhere( 1955 n => n.prop("data-l10n-id") === "newtab-topsites-url-validation" 1956 ).length, 1957 1 1958 ); 1959 }); 1960 1961 it("should reset the error state on value change", () => { 1962 wrapper.find("input").simulate("change", { target: { value: "bar" } }); 1963 1964 assert.isFalse(wrapper.state().validationError); 1965 }); 1966 }); 1967 });