ContentTiles.test.jsx (30085B)
1 import React from "react"; 2 import { shallow, mount } from "enzyme"; 3 import { ContentTiles } from "content-src/components/ContentTiles"; 4 import { ActionChecklist } from "content-src/components/ActionChecklist"; 5 import { MobileDownloads } from "content-src/components/MobileDownloads"; 6 import { EmbeddedBackupRestore } from "content-src/components/EmbeddedBackupRestore"; 7 import { EmbeddedMigrationWizard } from "content-src/components/EmbeddedMigrationWizard"; 8 import { AboutWelcomeUtils } from "content-src/lib/aboutwelcome-utils.mjs"; 9 import { GlobalOverrider } from "asrouter/tests/unit/utils"; 10 11 describe("ContentTiles component", () => { 12 let sandbox; 13 let wrapper; 14 let handleAction; 15 let setActiveMultiSelect; 16 let setActiveSingleSelectSelection; 17 let globals; 18 19 const CHECKLIST_TILE = { 20 type: "action_checklist", 21 header: { 22 title: "Checklist Header", 23 subtitle: "Checklist Subtitle", 24 style: { 25 border: "1px solid #ccc", 26 }, 27 }, 28 data: [ 29 { 30 id: "action-checklist-test", 31 targeting: "false", 32 label: { 33 raw: "Test label", 34 }, 35 action: { 36 data: { 37 pref: { 38 name: "messaging-system-action.test1", 39 value: "false", 40 }, 41 }, 42 type: "SET_PREF", 43 }, 44 }, 45 ], 46 }; 47 48 const MOBILE_TILE = { 49 type: "mobile_downloads", 50 header: { 51 title: "Mobile Header", 52 style: { 53 backgroundColor: "#e0e0e0", 54 border: "1px solid #999", 55 }, 56 }, 57 data: { 58 email: { 59 link_text: "Email yourself a link", 60 }, 61 }, 62 }; 63 64 const TITLE_TILE = { 65 type: "mobile_downloads", 66 title: "Tile Title", 67 subtitle: "Tile Subtitle", 68 data: { 69 email: { 70 link_text: "Email yourself a link", 71 }, 72 }, 73 }; 74 75 const TEST_CONTENT = { 76 tiles: [CHECKLIST_TILE, MOBILE_TILE], 77 }; 78 79 const EMBEDDED_BACKUP_RESTORE_TILE = { 80 type: "backup_restore", 81 title: "Tile Title", 82 subtitle: "Tile Subtitle", 83 }; 84 85 beforeEach(() => { 86 sandbox = sinon.createSandbox(); 87 handleAction = sandbox.stub(); 88 setActiveMultiSelect = sandbox.stub(); 89 setActiveSingleSelectSelection = sandbox.stub(); 90 globals = new GlobalOverrider(); 91 globals.set({ 92 AWSendToDeviceEmailsSupported: () => Promise.resolve(), 93 }); 94 globals.set({ AWSendToParent: sandbox.stub() }); 95 globals.set({ 96 AWSendToParent: sandbox.stub(), 97 AWFindBackupsInWellKnownLocations: sandbox.stub().resolves({ 98 found: false, 99 multipleBackupsFound: false, 100 backupFileToRestore: null, 101 }), 102 }); 103 wrapper = shallow( 104 <ContentTiles 105 content={TEST_CONTENT} 106 handleAction={handleAction} 107 activeMultiSelect={null} 108 setActiveMultiSelect={setActiveMultiSelect} 109 /> 110 ); 111 }); 112 113 afterEach(() => { 114 sandbox.restore(); 115 globals.restore(); 116 }); 117 118 it("should render the component when tiles are provided", () => { 119 assert.ok(wrapper.exists()); 120 }); 121 122 it("should not render the component when no tiles are provided", () => { 123 wrapper.setProps({ content: {} }); 124 assert.ok(wrapper.isEmptyRender()); 125 }); 126 127 it("should render the correct number of tiles", () => { 128 assert.equal(wrapper.find(".content-tile").length, 2); 129 }); 130 131 it("should toggle a tile and send telemetry when its header is clicked", () => { 132 let telemetrySpy = sandbox.spy(AboutWelcomeUtils, "sendActionTelemetry"); 133 const [firstTile] = TEST_CONTENT.tiles; 134 const tileId = `${firstTile.type}${firstTile.id ? "_" : ""}${ 135 firstTile.id ?? "" 136 }_header`; 137 const firstTileButton = wrapper.find(".tile-header").at(0); 138 firstTileButton.simulate("click"); 139 assert.equal( 140 wrapper.find(".tile-content").at(0).prop("id"), 141 "tile-content-0" 142 ); 143 assert.equal(wrapper.find(".tile-content").at(0).exists(), true); 144 assert.calledOnce(telemetrySpy); 145 assert.equal(telemetrySpy.firstCall.args[1], tileId); 146 }); 147 148 it("should only expand one tile at a time", () => { 149 const firstTileButton = wrapper.find(".tile-header").at(0); 150 firstTileButton.simulate("click"); 151 const secondTileButton = wrapper.find(".tile-header").at(1); 152 secondTileButton.simulate("click"); 153 154 assert.equal(wrapper.find(".tile-content").length, 1); 155 assert.equal( 156 wrapper.find(".tile-content").at(0).prop("id"), 157 "tile-content-1" 158 ); 159 }); 160 161 it("should toggle all tiles and send telemetry when the tiles header is clicked", () => { 162 const TEST_CONTENT_HEADER = { 163 tiles: [CHECKLIST_TILE, MOBILE_TILE], 164 tiles_header: { 165 title: "Toggle Tiles Header", 166 }, 167 }; 168 169 wrapper = mount( 170 <ContentTiles 171 content={TEST_CONTENT_HEADER} 172 handleAction={handleAction} 173 activeMultiSelect={null} 174 setActiveMultiSelect={setActiveMultiSelect} 175 /> 176 ); 177 178 let telemetrySpy = sandbox.spy(AboutWelcomeUtils, "sendActionTelemetry"); 179 const tilesHeaderButton = wrapper.find(".content-tiles-header"); 180 181 assert.ok(tilesHeaderButton.exists(), "Tiles header button should exist"); 182 tilesHeaderButton.simulate("click"); 183 184 assert.equal( 185 wrapper.find("#content-tiles-container").exists(), 186 true, 187 "Content tiles container should be visible after toggle" 188 ); 189 assert.calledOnce(telemetrySpy); 190 assert.equal( 191 telemetrySpy.firstCall.args[1], 192 "content_tiles_header", 193 "Telemetry should be sent for tiles header toggle" 194 ); 195 196 tilesHeaderButton.simulate("click"); 197 assert.equal( 198 wrapper.find("#content-tiles-container").exists(), 199 false, 200 "Content tiles container should not be visible after second toggle" 201 ); 202 }); 203 204 it("should apply configured styles to the header buttons", () => { 205 const mountedWrapper = mount( 206 <ContentTiles 207 content={TEST_CONTENT} 208 handleAction={() => {}} 209 activeMultiSelect={null} 210 setActiveMultiSelect={setActiveMultiSelect} 211 /> 212 ); 213 214 const firstTileHeader = mountedWrapper 215 .find(".tile-header") 216 .at(0) 217 .getDOMNode(); 218 const secondTileHeader = mountedWrapper 219 .find(".tile-header") 220 .at(1) 221 .getDOMNode(); 222 223 assert.equal( 224 firstTileHeader.style.cssText, 225 "border: 1px solid rgb(204, 204, 204);", 226 "First tile header styles should match configured values" 227 ); 228 assert.equal( 229 secondTileHeader.style.cssText, 230 "background-color: rgb(224, 224, 224); border: 1px solid rgb(153, 153, 153);", 231 "Second tile header styles should match configured values" 232 ); 233 234 mountedWrapper.unmount(); 235 }); 236 237 it("should render ActionChecklist for 'action_checklist' tile type", () => { 238 const firstTileButton = wrapper.find(".tile-header").at(0); 239 assert.ok(firstTileButton.exists(), "Tile header button should exist"); 240 firstTileButton.simulate("click"); 241 242 const actionChecklist = wrapper.find(ActionChecklist); 243 assert.ok(actionChecklist.exists()); 244 assert.deepEqual(actionChecklist.prop("content").tiles[0].data, [ 245 { 246 id: "action-checklist-test", 247 targeting: "false", 248 label: { 249 raw: "Test label", 250 }, 251 action: { 252 data: { 253 pref: { 254 name: "messaging-system-action.test1", 255 value: "false", 256 }, 257 }, 258 type: "SET_PREF", 259 }, 260 }, 261 ]); 262 }); 263 264 it("should render MobileDownloads for 'mobile_downloads' tile type", () => { 265 const secondTileButton = wrapper.find(".tile-header").at(1); 266 assert.ok(secondTileButton.exists(), "Tile header button should exist"); 267 secondTileButton.simulate("click"); 268 269 const mobileDownloads = wrapper.find(MobileDownloads); 270 assert.ok(mobileDownloads.exists()); 271 assert.deepEqual(mobileDownloads.prop("data"), { 272 email: { 273 link_text: "Email yourself a link", 274 }, 275 }); 276 assert.equal(mobileDownloads.prop("handleAction"), handleAction); 277 }); 278 279 it("should render EmbeddedBackupRestore for 'backup_restore' tile type", () => { 280 const TEST_CONTENT_WITH_EMBEDDED_BACKUP_RESTORE = { 281 tiles: [EMBEDDED_BACKUP_RESTORE_TILE], 282 }; 283 284 const backupWrapper = mount( 285 <ContentTiles 286 content={TEST_CONTENT_WITH_EMBEDDED_BACKUP_RESTORE} 287 handleAction={handleAction} 288 activeMultiSelect={null} 289 setActiveMultiSelect={setActiveMultiSelect} 290 /> 291 ); 292 293 const embeddedBackupRestore = backupWrapper.find(EmbeddedBackupRestore); 294 assert.ok( 295 embeddedBackupRestore.exists(), 296 "EmbeddedBackupRestore component should be rendered" 297 ); 298 299 backupWrapper.unmount(); 300 }); 301 302 it("should handle a single tile object", () => { 303 wrapper.setProps({ 304 content: { 305 tiles: { 306 type: "action_checklist", 307 data: [ 308 { 309 id: "action-checklist-single", 310 targeting: "false", 311 label: { 312 raw: "Single tile label", 313 }, 314 action: { 315 data: { 316 pref: { 317 name: "messaging-system-action.single", 318 value: "false", 319 }, 320 }, 321 type: "SET_PREF", 322 }, 323 }, 324 ], 325 }, 326 }, 327 }); 328 329 const actionChecklist = wrapper.find(ActionChecklist); 330 assert.ok(actionChecklist.exists()); 331 assert.deepEqual(actionChecklist.prop("content").tiles.data, [ 332 { 333 id: "action-checklist-single", 334 targeting: "false", 335 label: { 336 raw: "Single tile label", 337 }, 338 action: { 339 data: { 340 pref: { 341 name: "messaging-system-action.single", 342 value: "false", 343 }, 344 }, 345 type: "SET_PREF", 346 }, 347 }, 348 ]); 349 }); 350 351 it("should prefill activeMultiSelect for a MultiSelect tile based on default values", () => { 352 const MULTISELECT_TILE = { 353 type: "multiselect", 354 header: { 355 title: "Multiselect Header", 356 style: { 357 border: "1px solid #ddd", 358 }, 359 }, 360 data: [ 361 { id: "option1", defaultValue: true }, 362 { id: "option2", defaultValue: false }, 363 { id: "option3", defaultValue: true }, 364 ], 365 }; 366 367 const contentWithMultiselect = { tiles: MULTISELECT_TILE }; 368 369 wrapper = mount( 370 <ContentTiles 371 content={contentWithMultiselect} 372 activeMultiSelect={null} 373 setActiveMultiSelect={setActiveMultiSelect} 374 handleAction={handleAction} 375 /> 376 ); 377 wrapper.update(); 378 379 sinon.assert.calledOnce(setActiveMultiSelect); 380 sinon.assert.calledWithExactly( 381 setActiveMultiSelect, 382 ["option1", "option3"], 383 "tile-0" 384 ); 385 }); 386 387 it("should not prefill activeMultiSelect if it is already set", () => { 388 const MULTISELECT_TILE = { 389 type: "multiselect", 390 header: { 391 title: "Multiselect Header", 392 style: { 393 border: "1px solid #ddd", 394 }, 395 }, 396 data: [ 397 { id: "option1", defaultValue: true }, 398 { id: "option2", defaultValue: false }, 399 { id: "option3", defaultValue: true }, 400 ], 401 }; 402 403 const contentWithMultiselect = { tiles: [MULTISELECT_TILE] }; 404 405 wrapper = mount( 406 <ContentTiles 407 content={contentWithMultiselect} 408 activeMultiSelect={["option2"]} 409 setActiveMultiSelect={setActiveMultiSelect} 410 handleAction={handleAction} 411 /> 412 ); 413 wrapper.update(); 414 415 sinon.assert.notCalled(setActiveMultiSelect); 416 }); 417 418 it("should render title and subtitle if present", () => { 419 sandbox.stub(window, "AWSendToDeviceEmailsSupported").resolves(true); 420 421 let TEST_TILE_CONTENT = { 422 tiles: [TITLE_TILE], 423 }; 424 425 const mountedWrapper = mount( 426 <ContentTiles 427 content={TEST_TILE_CONTENT} 428 handleAction={() => {}} 429 activeMultiSelect={null} 430 setActiveMultiSelect={setActiveMultiSelect} 431 /> 432 ); 433 434 const tileTitle = mountedWrapper.find(".tile-title"); 435 const tileSubtitle = mountedWrapper.find(".tile-subtitle"); 436 437 assert.ok(tileTitle.exists(), "Title should render"); 438 assert.ok(tileSubtitle.exists(), "Subtitle should render"); 439 440 assert.equal( 441 tileTitle.text(), 442 "Tile Title", 443 "Tile title should have correct text" 444 ); 445 assert.equal( 446 tileSubtitle.text(), 447 "Tile Subtitle", 448 "Tile subtitle should have correct text" 449 ); 450 451 mountedWrapper.unmount(); 452 }); 453 454 it("should render multiple title and subtitles if multiple tiles contain them", () => { 455 sandbox.stub(window, "AWSendToDeviceEmailsSupported").resolves(true); 456 const SECOND_TITLE_TILE = { 457 type: "mobile_downloads", 458 title: "Tile Title 2", 459 subtitle: "Tile Subtitle 2", 460 data: { 461 email: { 462 link_text: "Email yourself a link", 463 }, 464 }, 465 }; 466 467 let MULTIPLE_TILES_CONTENT = { 468 tiles: [TITLE_TILE, SECOND_TITLE_TILE], 469 }; 470 471 const mountedWrapper = mount( 472 <ContentTiles 473 content={MULTIPLE_TILES_CONTENT} 474 handleAction={() => {}} 475 activeMultiSelect={null} 476 setActiveMultiSelect={setActiveMultiSelect} 477 setActiveSingleSelectSelection={setActiveSingleSelectSelection} 478 /> 479 ); 480 481 const tileTitles = mountedWrapper.find(".tile-title"); 482 const tileSubtitles = mountedWrapper.find(".tile-subtitle"); 483 484 assert.equal(tileTitles.length, 2, "Should render two tile titles"); 485 assert.equal(tileSubtitles.length, 2, "Should render two tile subtitles"); 486 487 assert.equal( 488 tileTitles.at(0).text(), 489 "Tile Title", 490 "First tile title should have correct text" 491 ); 492 assert.equal( 493 tileSubtitles.at(0).text(), 494 "Tile Subtitle", 495 "First tile subtitle should have correct text" 496 ); 497 498 assert.equal( 499 tileTitles.at(1).text(), 500 "Tile Title 2", 501 "Second tile title should have correct text" 502 ); 503 assert.equal( 504 tileSubtitles.at(1).text(), 505 "Tile Subtitle 2", 506 "Second tile subtitle should have correct text" 507 ); 508 509 mountedWrapper.unmount(); 510 }); 511 512 it("should pre-populate multiple MultiSelect components in the same ContentTiles independently of one another", () => { 513 const FIRST_MULTISELECT_TILE = { 514 type: "multiselect", 515 header: { 516 title: "First Multiselect", 517 }, 518 data: [ 519 { id: "checklist1-option1", defaultValue: true }, 520 { id: "checklist1-option2", defaultValue: false }, 521 ], 522 }; 523 524 const SECOND_MULTISELECT_TILE = { 525 type: "multiselect", 526 header: { 527 title: "Second Multiselect", 528 }, 529 data: [ 530 { id: "checklist2-option1", defaultValue: false }, 531 { id: "checklist2-option2", defaultValue: true }, 532 ], 533 }; 534 535 const contentWithMultipleMultiselects = { 536 tiles: [FIRST_MULTISELECT_TILE, SECOND_MULTISELECT_TILE], 537 }; 538 539 wrapper = mount( 540 <ContentTiles 541 content={contentWithMultipleMultiselects} 542 activeMultiSelect={null} 543 setActiveMultiSelect={setActiveMultiSelect} 544 handleAction={handleAction} 545 /> 546 ); 547 wrapper.update(); 548 549 sinon.assert.calledWithExactly( 550 setActiveMultiSelect.getCall(0), 551 ["checklist1-option1"], 552 "tile-0" 553 ); 554 sinon.assert.calledWithExactly( 555 setActiveMultiSelect.getCall(1), 556 ["checklist2-option2"], 557 "tile-1" 558 ); 559 560 wrapper.unmount(); 561 }); 562 563 it("should handle interaction between multiselect instances independently of one another", () => { 564 const FIRST_MULTISELECT_TILE = { 565 type: "multiselect", 566 header: { 567 title: "First Multiselect", 568 }, 569 data: [ 570 { id: "checklist1-option1", defaultValue: true, label: "Option 1-1" }, 571 { id: "checklist1-option2", defaultValue: false, label: "Option 1-2" }, 572 ], 573 }; 574 575 const SECOND_MULTISELECT_TILE = { 576 type: "multiselect", 577 header: { 578 title: "Second Multiselect", 579 }, 580 data: [ 581 { id: "checklist2-option1", defaultValue: false, label: "Option 2-1" }, 582 { id: "checklist2-option2", defaultValue: true, label: "Option 2-2" }, 583 ], 584 }; 585 586 const contentWithMultipleMultiselects = { 587 tiles: [FIRST_MULTISELECT_TILE, SECOND_MULTISELECT_TILE], 588 }; 589 590 const initialActiveMultiSelect = { 591 "tile-0": ["checklist1-option1"], 592 "tile-1": ["checklist2-option2"], 593 }; 594 595 const setScreenMultiSelects = sandbox.stub(); 596 597 wrapper = mount( 598 <ContentTiles 599 content={contentWithMultipleMultiselects} 600 activeMultiSelect={initialActiveMultiSelect} 601 setActiveMultiSelect={setActiveMultiSelect} 602 setScreenMultiSelects={setScreenMultiSelects} 603 handleAction={handleAction} 604 /> 605 ); 606 607 // First multiselect 608 const firstTileButton = wrapper.find(".tile-header").at(0); 609 assert.ok(firstTileButton.exists(), "First tile header should exist"); 610 firstTileButton.simulate("click"); 611 612 const firstChecklistInputs = wrapper.find(".multi-select-container input"); 613 assert.equal( 614 firstChecklistInputs.length, 615 2, 616 "First checklist should have 2 inputs" 617 ); 618 619 assert.equal( 620 firstChecklistInputs.at(0).prop("checked"), 621 true, 622 "First option should be checked" 623 ); 624 assert.equal( 625 firstChecklistInputs.at(1).prop("checked"), 626 false, 627 "Second option should be unchecked" 628 ); 629 630 const secondInput = firstChecklistInputs.at(1); 631 secondInput.getDOMNode().checked = true; 632 secondInput.simulate("change"); 633 634 sinon.assert.calledOnce(setActiveMultiSelect); 635 sinon.assert.calledWithExactly( 636 setActiveMultiSelect, 637 ["checklist1-option1", "checklist1-option2"], 638 "tile-0" 639 ); 640 setActiveMultiSelect.reset(); 641 642 // Second multiSelect 643 const secondTileButton = wrapper.find(".tile-header").at(1); 644 secondTileButton.simulate("click"); 645 646 const secondChecklistInputs = wrapper.find(".multi-select-container input"); 647 assert.equal( 648 secondChecklistInputs.length, 649 2, 650 "Second checklist should have 2 inputs" 651 ); 652 653 assert.equal( 654 secondChecklistInputs.at(0).prop("checked"), 655 false, 656 "First option should be unchecked" 657 ); 658 assert.equal( 659 secondChecklistInputs.at(1).prop("checked"), 660 true, 661 "Second option should be checked" 662 ); 663 664 // Uncheck the second checkbox in the second multiselect 665 const lastInput = secondChecklistInputs.at(1); 666 lastInput.getDOMNode().checked = false; 667 lastInput.simulate("change"); 668 669 sinon.assert.calledOnce(setActiveMultiSelect); 670 sinon.assert.calledWithExactly(setActiveMultiSelect, [], "tile-1"); 671 672 // Ensure the first multiselect's state wasn't altered by changes to the second multiselect 673 wrapper.setProps({ 674 activeMultiSelect: { 675 "tile-0": ["checklist1-option1", "checklist1-option2"], 676 "tile-1": [], 677 }, 678 }); 679 680 firstTileButton.simulate("click"); 681 682 // Get the updated checkboxes from the first checklist 683 const updatedFirstInputs = wrapper.find(".multi-select-container input"); 684 685 assert.equal( 686 updatedFirstInputs.at(0).prop("checked"), 687 true, 688 "First option should still be checked" 689 ); 690 assert.equal( 691 updatedFirstInputs.at(1).prop("checked"), 692 true, 693 "Second option should still be checked" 694 ); 695 696 wrapper.unmount(); 697 }); 698 699 it("should select defaults of single select tiles independently of one another", () => { 700 const SINGLE_SELECT_1 = { 701 type: "single-select", 702 selected: "test1", 703 data: [ 704 { 705 id: "test1", 706 label: { 707 raw: "test1 label", 708 }, 709 }, 710 { 711 defaultValue: true, 712 id: "test2", 713 label: { 714 raw: "test2 label", 715 }, 716 }, 717 ], 718 }; 719 720 const SINGLE_SELECT_2 = { 721 type: "single-select", 722 selected: "test4", 723 data: [ 724 { 725 id: "test3", 726 label: { 727 raw: "test3 label", 728 }, 729 }, 730 { 731 defaultValue: true, 732 id: "test4", 733 label: { 734 raw: "test4 label", 735 }, 736 }, 737 ], 738 }; 739 740 const content = { tiles: [SINGLE_SELECT_1, SINGLE_SELECT_2] }; 741 wrapper = mount( 742 <ContentTiles 743 content={content} 744 setActiveSingleSelectSelection={setActiveSingleSelectSelection} 745 handleAction={handleAction} 746 /> 747 ); 748 wrapper.update(); 749 750 sinon.assert.calledWithExactly( 751 setActiveSingleSelectSelection.getCall(0), 752 "test1", 753 "single-select-0" 754 ); 755 756 sinon.assert.calledWithExactly( 757 setActiveSingleSelectSelection.getCall(1), 758 "test4", 759 "single-select-1" 760 ); 761 wrapper.unmount(); 762 }); 763 764 it("should handle interactions with multiple single select tiles independently of one another", () => { 765 const SINGLE_SELECT_1 = { 766 type: "single-select", 767 selected: "test1", 768 data: [ 769 { 770 id: "test1", 771 label: { 772 raw: "test1 label", 773 }, 774 }, 775 { 776 defaultValue: true, 777 id: "test2", 778 label: { 779 raw: "test2 label", 780 }, 781 }, 782 ], 783 }; 784 785 const SINGLE_SELECT_2 = { 786 type: "single-select", 787 selected: "test4", 788 data: [ 789 { 790 id: "test3", 791 label: { 792 raw: "test3 label", 793 }, 794 }, 795 { 796 defaultValue: true, 797 id: "test4", 798 label: { 799 raw: "test4 label", 800 }, 801 }, 802 ], 803 }; 804 805 const content = { tiles: [SINGLE_SELECT_1, SINGLE_SELECT_2] }; 806 wrapper = mount( 807 <ContentTiles 808 content={content} 809 setActiveSingleSelectSelection={setActiveSingleSelectSelection} 810 handleAction={handleAction} 811 /> 812 ); 813 814 wrapper.update(); 815 816 const tile2 = wrapper.find('input[value="test2"]'); 817 tile2.simulate("click"); 818 819 sinon.assert.calledWithExactly( 820 setActiveSingleSelectSelection.getCall(2), 821 "test2", 822 "single-select-0" 823 ); 824 825 const tile3 = wrapper.find('input[value="test3"]'); 826 tile3.simulate("click"); 827 828 sinon.assert.calledWithExactly( 829 setActiveSingleSelectSelection.getCall(3), 830 "test3", 831 "single-select-1" 832 ); 833 wrapper.unmount(); 834 }); 835 836 it("should apply styles to label element", () => { 837 const TEST_STYLE = { marginBlock: "5px" }; 838 const tileData = { 839 type: "single-select", 840 selected: "vertical", 841 data: [ 842 { 843 id: "vertical", 844 label: { raw: "Vertical", marginBlock: "5px" }, 845 }, 846 ], 847 }; 848 849 wrapper = mount( 850 <ContentTiles 851 content={{ tiles: [tileData] }} 852 setActiveSingleSelectSelection={() => {}} 853 handleAction={() => {}} 854 /> 855 ); 856 857 const styledDiv = wrapper.find(".text").at(0); 858 859 assert.deepEqual( 860 styledDiv.prop("style"), 861 TEST_STYLE, 862 "Style prop should match TEST_STYLE" 863 ); 864 wrapper.unmount(); 865 }); 866 867 it("should apply valid styles from tile.data.style and include minWidth from icon.width", () => { 868 const icon = { 869 width: "101px", 870 }; 871 872 const style = { 873 paddingBlock: "8px", 874 }; 875 876 const tileData = { 877 type: "single-select", 878 selected: "test", 879 data: [ 880 { 881 id: "test", 882 icon, 883 label: { raw: "Test" }, 884 style, 885 }, 886 ], 887 }; 888 889 wrapper = mount( 890 <ContentTiles 891 content={{ tiles: [tileData] }} 892 setActiveSingleSelectSelection={() => {}} 893 handleAction={() => {}} 894 /> 895 ); 896 897 const label = wrapper.find("label.select-item").at(0); 898 const labelStyle = label.prop("style"); 899 900 assert.equal( 901 labelStyle.paddingBlock, 902 "8px", 903 "paddingBlock should be applied" 904 ); 905 assert.equal( 906 labelStyle.minWidth, 907 "101px", 908 "minWidth should be set from icon.width" 909 ); 910 wrapper.unmount(); 911 }); 912 913 it("restores last tiles focus in Spotlight context and genuine Tab is ignored", async () => { 914 const TAB_GRACE_WINDOW_MS = 250; 915 916 function nextFrame() { 917 return new Promise(r => requestAnimationFrame(r)); 918 } 919 function delay(ms) { 920 return new Promise(r => setTimeout(r, ms)); 921 } 922 async function waitFor(condition, timeout = TAB_GRACE_WINDOW_MS) { 923 const start = performance.now(); 924 while (!condition()) { 925 await nextFrame(); 926 if (performance.now() - start > timeout) { 927 throw new Error("timeout waiting for condition"); 928 } 929 } 930 } 931 932 // Pretend we're in a Spotlight dialog so the effect runs 933 const root = document.body.appendChild(document.createElement("div")); 934 root.id = "multi-stage-message-root"; 935 root.className = "onboardingContainer"; 936 root.dataset.page = "spotlight"; 937 938 const mountNode = document.body.appendChild(document.createElement("div")); 939 940 const content = { 941 tiles: [{ type: "multiselect", header: { title: "Test" }, data: [] }], 942 }; 943 944 const focusWrapper = mount( 945 <main role="alertdialog"> 946 <ContentTiles 947 content={content} 948 handleAction={() => {}} 949 activeMultiSelect={null} 950 setActiveMultiSelect={() => {}} 951 /> 952 <div className="action-buttons"> 953 <button className="primary">Continue</button> 954 </div> 955 </main>, 956 { attachTo: mountNode } 957 ); 958 959 // Let the hook attach listeners 960 await nextFrame(); 961 962 const dialog = focusWrapper.getDOMNode(); 963 const header = dialog.querySelector(".tile-header"); 964 const primary = dialog.querySelector(".action-buttons .primary"); 965 966 // Record real DOM focus inside tiles 967 header.focus(); 968 header.dispatchEvent(new FocusEvent("focusin", { bubbles: true })); 969 await nextFrame(); 970 971 // Wait past the tab grace window so this isn’t treated as a real Tab 972 await delay(TAB_GRACE_WINDOW_MS + 1); 973 974 // Simulate programmatic focus “snap” to an outside control 975 primary.focus(); 976 primary.dispatchEvent(new FocusEvent("focusin", { bubbles: true })); 977 978 await waitFor(() => document.activeElement === header); 979 assert.strictEqual( 980 document.activeElement, 981 header, 982 "restored focus to tiles header" 983 ); 984 985 dialog.dispatchEvent( 986 new KeyboardEvent("keydown", { 987 key: "Tab", 988 bubbles: true, 989 cancelable: true, 990 }) 991 ); 992 primary.focus(); 993 primary.dispatchEvent(new FocusEvent("focusin", { bubbles: true })); 994 await nextFrame(); 995 assert.strictEqual( 996 document.activeElement, 997 primary, 998 "did not override genuine Tab focus" 999 ); 1000 1001 // Cleanup 1002 focusWrapper.unmount(); 1003 mountNode.remove(); 1004 root.remove(); 1005 }); 1006 1007 it("passes content.skip_button to EmbeddedBackupRestore as skipButton", () => { 1008 const content = { 1009 tiles: [EMBEDDED_BACKUP_RESTORE_TILE], 1010 skip_button: { 1011 label: { raw: "Don't restore" }, 1012 action: { navigate: true }, 1013 }, 1014 }; 1015 1016 const mountedWrapper = mount( 1017 <ContentTiles 1018 content={content} 1019 handleAction={handleAction} 1020 activeMultiSelect={null} 1021 setActiveMultiSelect={setActiveMultiSelect} 1022 /> 1023 ); 1024 1025 const embeddedBackupComponent = mountedWrapper.find(EmbeddedBackupRestore); 1026 assert.ok( 1027 embeddedBackupComponent.exists(), 1028 "EmbeddedBackupRestore rendered" 1029 ); 1030 assert.deepEqual( 1031 embeddedBackupComponent.prop("skipButton"), 1032 content.skip_button, 1033 "prop is wired" 1034 ); 1035 mountedWrapper.unmount(); 1036 }); 1037 1038 it("renders a single tile without container when there is no header or container style", () => { 1039 const SINGLE_TILE_NO_CONTAINER = { 1040 tiles: { 1041 tile_items: { 1042 type: "mobile_downloads", 1043 data: { email: { link_text: "Email!" } }, 1044 }, 1045 container: {}, 1046 }, 1047 }; 1048 1049 const mounted = mount( 1050 <ContentTiles 1051 content={SINGLE_TILE_NO_CONTAINER} 1052 handleAction={() => {}} 1053 activeMultiSelect={null} 1054 setActiveMultiSelect={setActiveMultiSelect} 1055 /> 1056 ); 1057 1058 assert.isFalse( 1059 mounted.find("#content-tiles-container").exists(), 1060 "should not render container wrapper" 1061 ); 1062 assert.equal(mounted.find(".content-tile").length, 1); 1063 1064 mounted.unmount(); 1065 }); 1066 1067 it("passes migration_wizard_options properties to migration-wizard element", () => { 1068 const MIGRATION_WIZARD_TILE = { 1069 type: "migration-wizard", 1070 migration_wizard_options: { 1071 force_show_import_all: true, 1072 option_expander_title_string: "Custom title", 1073 hide_option_expander_subtitle: true, 1074 hide_select_all: true, 1075 }, 1076 }; 1077 1078 const content = { 1079 tiles: [MIGRATION_WIZARD_TILE], 1080 }; 1081 1082 const mountedWrapper = mount( 1083 <ContentTiles 1084 content={content} 1085 handleAction={handleAction} 1086 activeMultiSelect={null} 1087 setActiveMultiSelect={setActiveMultiSelect} 1088 /> 1089 ); 1090 1091 const embeddedMigrationWizard = mountedWrapper.find( 1092 EmbeddedMigrationWizard 1093 ); 1094 assert.ok( 1095 embeddedMigrationWizard.exists(), 1096 "EmbeddedMigrationWizard rendered" 1097 ); 1098 1099 const migrationWizardEl = mountedWrapper.find("migration-wizard"); 1100 assert.ok(migrationWizardEl.exists(), "migration-wizard element rendered"); 1101 1102 assert.equal( 1103 migrationWizardEl.prop("force-show-import-all"), 1104 true, 1105 "force-show-import-all is set" 1106 ); 1107 assert.equal( 1108 migrationWizardEl.prop("option-expander-title-string"), 1109 "Custom title", 1110 "option-expander-title-string is set" 1111 ); 1112 assert.equal( 1113 migrationWizardEl.prop("hide-option-expander-subtitle"), 1114 true, 1115 "hide-option-expander-subtitle is set" 1116 ); 1117 assert.equal( 1118 migrationWizardEl.prop("hide-select-all"), 1119 true, 1120 "hide-select-all is set" 1121 ); 1122 1123 mountedWrapper.unmount(); 1124 }); 1125 });