github.com/tilt-dev/tilt@v0.36.0/web/src/OverviewTable.test.tsx (about) 1 import { render, screen } from "@testing-library/react" 2 import userEvent from "@testing-library/user-event" 3 import { SnackbarProvider } from "notistack" 4 import React, { ReactElement } from "react" 5 import { MemoryRouter } from "react-router" 6 import { ApiButtonRoot } from "./ApiButton" 7 import Features, { FeaturesTestProvider, Flag } from "./feature" 8 import { GroupByLabelView, TILTFILE_LABEL, UNLABELED_LABEL } from "./labels" 9 import LogStore from "./LogStore" 10 import OverviewTable, { 11 labeledResourcesToTableCells, 12 NoMatchesFound, 13 OverviewGroup, 14 OverviewGroupName, 15 ResourceResultCount, 16 ResourceTableData, 17 ResourceTableHeaderSortTriangle, 18 ResourceTableRow, 19 TableGroupedByLabels, 20 } from "./OverviewTable" 21 import { Name, RowValues, SelectionCheckbox } from "./OverviewTableColumns" 22 import { ToggleTriggerModeTooltip } from "./OverviewTableTriggerModeToggle" 23 import { 24 DEFAULT_GROUP_STATE, 25 GroupsState, 26 ResourceGroupsContextProvider, 27 } from "./ResourceGroupsContext" 28 import { 29 DEFAULT_OPTIONS, 30 ResourceListOptions, 31 ResourceListOptionsProvider, 32 } from "./ResourceListOptionsContext" 33 import { matchesResourceName } from "./ResourceNameFilter" 34 import { ResourceSelectionProvider } from "./ResourceSelectionContext" 35 import { ResourceStatusSummaryRoot } from "./ResourceStatusSummary" 36 import { 37 nResourceView, 38 nResourceWithLabelsView, 39 oneResource, 40 oneUIButton, 41 TestDataView, 42 } from "./testdata" 43 import { RuntimeStatus, TriggerMode, UpdateStatus } from "./types" 44 45 // Helpers 46 const tableViewWithSettings = ({ 47 view, 48 labelsEnabled, 49 resourceListOptions, 50 resourceSelections, 51 }: { 52 view: TestDataView 53 labelsEnabled?: boolean 54 resourceListOptions?: ResourceListOptions 55 resourceSelections?: string[] 56 }) => { 57 const features = new Features({ 58 [Flag.Labels]: labelsEnabled ?? true, 59 }) 60 return ( 61 <MemoryRouter initialEntries={["/"]}> 62 <SnackbarProvider> 63 <FeaturesTestProvider value={features}> 64 <ResourceGroupsContextProvider> 65 <ResourceListOptionsProvider 66 initialValuesForTesting={resourceListOptions} 67 > 68 <ResourceSelectionProvider 69 initialValuesForTesting={resourceSelections} 70 > 71 <OverviewTable view={view} /> 72 </ResourceSelectionProvider> 73 </ResourceListOptionsProvider> 74 </ResourceGroupsContextProvider> 75 </FeaturesTestProvider> 76 </SnackbarProvider> 77 </MemoryRouter> 78 ) 79 } 80 81 const findTableHeaderByName = (columnName: string, sortable = true): any => { 82 const selector = sortable ? `Sort by ${columnName}` : columnName 83 return screen.getAllByTitle(selector)[0] 84 } 85 86 // End helpers 87 88 afterEach(() => { 89 sessionStorage.clear() 90 localStorage.clear() 91 }) 92 93 it("shows buttons on the appropriate resources", () => { 94 let view = nResourceView(3) 95 // one resource with one button, one with multiple, and one with none 96 view.uiButtons = [ 97 oneUIButton({ 98 buttonName: "button1", 99 buttonText: "text1", 100 componentID: view.uiResources[0].metadata?.name!, 101 }), 102 oneUIButton({ 103 buttonName: "button2", 104 buttonText: "text2", 105 componentID: view.uiResources[1].metadata?.name!, 106 }), 107 oneUIButton({ 108 buttonName: "button3", 109 buttonText: "text3", 110 componentID: view.uiResources[1].metadata?.name!, 111 }), 112 ] 113 114 const { container } = render(tableViewWithSettings({ view })) 115 116 // buttons expected to be on each row, in order 117 const expectedButtons = [["text1"], ["text2", "text3"], []] 118 // first row is headers, so skip it 119 const rows = Array.from(container.querySelectorAll(ResourceTableRow)).slice(1) 120 const actualButtons = rows.map((row) => 121 Array.from(row.querySelectorAll(ApiButtonRoot)).map((e) => 122 e.getAttribute("aria-label") 123 ) 124 ) 125 126 expect(actualButtons).toEqual(expectedButtons) 127 }) 128 129 it("sorts by status", () => { 130 let view = nResourceView(10) 131 view.uiResources[3].status!.updateStatus = UpdateStatus.Error 132 view.uiResources[7].status!.runtimeStatus = RuntimeStatus.Error 133 view.uiResources.unshift( 134 oneResource({ disabled: true, name: "disabled_resource" }) 135 ) 136 137 const { container } = render( 138 tableViewWithSettings({ 139 view, 140 resourceListOptions: { ...DEFAULT_OPTIONS, showDisabledResources: true }, 141 }) 142 ) 143 144 const statusHeader = screen.getByText("Status") 145 userEvent.click(statusHeader) 146 147 const rows = Array.from(container.querySelectorAll(ResourceTableRow)).slice(1) // skip the header 148 const actualResources = rows.map( 149 (row) => 150 row.querySelectorAll(ResourceTableData)[4].querySelector("button")! 151 .textContent 152 ) 153 // 3 and 7 go first because they're failing, then it's alpha, 154 // followed by disabled resources 155 const expectedResources = [ 156 "_3", 157 "_7", 158 "(Tiltfile)", 159 "_1", 160 "_2", 161 "_4", 162 "_5", 163 "_6", 164 "_8", 165 "_9", 166 "disabled_resource", 167 ] 168 expect(expectedResources).toEqual(actualResources) 169 }) 170 171 describe("resource name filter", () => { 172 describe("when a filter is applied", () => { 173 let view: TestDataView 174 let container: HTMLElement 175 176 beforeEach(() => { 177 view = nResourceView(100) 178 container = renderContainer( 179 tableViewWithSettings({ 180 view, 181 resourceListOptions: { 182 ...DEFAULT_OPTIONS, 183 resourceNameFilter: "1", 184 }, 185 }) 186 ) 187 }) 188 189 it("displays an accurate result count", () => { 190 const resultCount = container.querySelector(ResourceResultCount) 191 expect(resultCount).toBeDefined() 192 // Expect 19 results because test data names resources with their index number 193 expect(resultCount!.textContent).toMatch(/19/) 194 }) 195 196 it("displays only matching resources if there are matches", () => { 197 const matchingRows = container.querySelectorAll("table tr") 198 199 // Expect 20 results because test data names resources with their index number 200 expect(matchingRows.length).toBe(20) 201 202 const displayedResourceNames = Array.from( 203 container.querySelectorAll(Name) 204 ).map((nameCell: any) => nameCell.textContent) 205 const everyNameMatchesFilterTerm = displayedResourceNames.every((name) => 206 matchesResourceName(name, "1") 207 ) 208 209 expect(everyNameMatchesFilterTerm).toBe(true) 210 }) 211 212 it("displays a `no matches` message if there are no matches", () => { 213 container = renderContainer( 214 tableViewWithSettings({ 215 view, 216 resourceListOptions: { 217 ...DEFAULT_OPTIONS, 218 resourceNameFilter: "eek no matches!", 219 }, 220 }) 221 ) 222 223 expect(container.querySelector(NoMatchesFound)).toBeDefined() 224 }) 225 }) 226 227 describe("when a filter is NOT applied", () => { 228 it("displays all resources", () => { 229 const { container } = render( 230 tableViewWithSettings({ 231 view: nResourceView(10), 232 resourceListOptions: { ...DEFAULT_OPTIONS }, 233 }) 234 ) 235 236 expect( 237 container.querySelectorAll(`tbody ${ResourceTableRow}`).length 238 ).toBe(10) 239 }) 240 }) 241 }) 242 243 describe("when labels feature is enabled", () => { 244 it("it displays tables grouped by labels if resources have labels", () => { 245 const { container } = render( 246 tableViewWithSettings({ 247 view: nResourceWithLabelsView(5), 248 labelsEnabled: true, 249 }) 250 ) 251 252 let labels = Array.from(container.querySelectorAll(OverviewGroupName)).map( 253 (n) => n.textContent 254 ) 255 expect(labels).toEqual([ 256 "backend", 257 "frontend", 258 "javascript", 259 "test", 260 "very_long_long_long_label", 261 "unlabeled", 262 "Tiltfile", 263 ]) 264 }) 265 266 it("it displays a single table if no resources have labels", () => { 267 const { container } = render( 268 tableViewWithSettings({ view: nResourceView(5), labelsEnabled: true }) 269 ) 270 271 let labels = Array.from(container.querySelectorAll(OverviewGroupName)).map( 272 (n) => n.textContent 273 ) 274 expect(labels).toEqual([]) 275 }) 276 277 it("it displays the resource grouping tooltip if no resources have labels", () => { 278 const { container } = render( 279 tableViewWithSettings({ view: nResourceView(5), labelsEnabled: true }) 280 ) 281 282 expect( 283 container.querySelectorAll('#table-groups-info[role="tooltip"]').length 284 ).toBe(1) 285 }) 286 }) 287 288 describe("when labels feature is NOT enabled", () => { 289 let container: HTMLElement 290 291 beforeEach(() => { 292 container = renderContainer( 293 tableViewWithSettings({ 294 view: nResourceWithLabelsView(5), 295 labelsEnabled: false, 296 }) 297 ) 298 }) 299 300 it("it displays a single table", () => { 301 expect(container.querySelectorAll("table").length).toBe(1) 302 }) 303 304 it("it does not display the resource grouping tooltip", () => { 305 expect(container.querySelectorAll(".MuiTooltip").length).toBe(0) 306 }) 307 }) 308 309 describe("overview table without groups", () => { 310 let view: TestDataView 311 let container: HTMLElement 312 313 beforeEach(() => { 314 view = nResourceView(8) 315 container = renderContainer( 316 tableViewWithSettings({ view, labelsEnabled: true }) 317 ) 318 }) 319 320 describe("sorting", () => { 321 it("table column header displays ascending arrow when sorted ascending", () => { 322 userEvent.click(findTableHeaderByName("Pod ID")) 323 const arrowIcon = findTableHeaderByName("Pod ID").querySelector( 324 ResourceTableHeaderSortTriangle 325 ) 326 327 expect(arrowIcon.classList.contains("is-sorted-asc")).toBe(true) 328 }) 329 330 it("table column header displays descending arrow when sorted descending", () => { 331 userEvent.click(findTableHeaderByName("Pod ID")) 332 userEvent.click(findTableHeaderByName("Pod ID")) 333 const arrowIcon = findTableHeaderByName("Pod ID").querySelector( 334 ResourceTableHeaderSortTriangle 335 ) 336 337 expect(arrowIcon.classList.contains("is-sorted-desc")).toBe(true) 338 }) 339 }) 340 }) 341 342 describe("overview table with groups", () => { 343 let view: TestDataView 344 let container: HTMLElement 345 let resources: GroupByLabelView<RowValues> 346 347 beforeEach(() => { 348 view = nResourceWithLabelsView(8) 349 container = renderContainer( 350 tableViewWithSettings({ view, labelsEnabled: true }) 351 ) 352 resources = labeledResourcesToTableCells( 353 view.uiResources, 354 view.uiButtons, 355 new LogStore() 356 ) 357 358 sessionStorage.clear() 359 localStorage.clear() 360 }) 361 362 afterEach(() => { 363 sessionStorage.clear() 364 localStorage.clear() 365 }) 366 367 describe("display", () => { 368 it("does not show the resource groups tooltip", () => { 369 expect(container.querySelectorAll(".MuiTooltip").length).toBe(0) 370 }) 371 372 it("renders each label group in order", () => { 373 const { labels: sortedLabels } = resources 374 const groupNames = container.querySelectorAll(OverviewGroupName) 375 376 // Loop through the sorted labels (which includes every label 377 // attached to a resource, but not unlabeled or tiltfile "labels") 378 sortedLabels.forEach((label, idx) => { 379 const groupName = groupNames[idx] 380 expect(groupName.textContent).toBe(label) 381 }) 382 }) 383 384 // Note: the sample data generated in the test helper `nResourcesWithLabels` 385 // always includes unlabeled resources 386 it("renders a resource group for unlabeled resources and for Tiltfiles", () => { 387 const groupNames = Array.from( 388 container.querySelectorAll(OverviewGroupName) 389 ) 390 expect(screen.getAllByText(UNLABELED_LABEL)).toBeTruthy() 391 expect(screen.getAllByText(TILTFILE_LABEL)).toBeTruthy() 392 }) 393 394 it("renders a table for each resource group", () => { 395 const tables = container.querySelectorAll("table") 396 const totalLabelCount = 397 resources.labels.length + 398 (resources.tiltfile.length ? 1 : 0) + 399 (resources.unlabeled.length ? 1 : 0) 400 401 expect(tables.length).toBe(totalLabelCount) 402 }) 403 404 it("renders the correct resources in each label group", () => { 405 const { labelsToResources, unlabeled, tiltfile } = resources 406 const resourceGroups = container.querySelectorAll(OverviewGroup) 407 408 const actualResourcesFromTable: { [key: string]: string[] } = {} 409 const expectedResourcesFromLabelGroups: { [key: string]: string[] } = {} 410 411 // Create a dictionary of labels to a list of resource names 412 // based on the view 413 Object.keys(labelsToResources).forEach((label) => { 414 const resourceNames = labelsToResources[label].map((r) => r.name) 415 expectedResourcesFromLabelGroups[label] = resourceNames 416 }) 417 418 expectedResourcesFromLabelGroups[UNLABELED_LABEL] = unlabeled.map( 419 (r) => r.name 420 ) 421 expectedResourcesFromLabelGroups[TILTFILE_LABEL] = tiltfile.map( 422 (r) => r.name 423 ) 424 425 // Create a dictionary of labels to a list of resource names 426 // based on what's rendered in each group table 427 resourceGroups.forEach((group: any) => { 428 // Find the label group name 429 const groupName = group.querySelector(OverviewGroupName).textContent 430 // Find the resource list displayed in the table 431 const table = group.querySelector("table") 432 const resourcesInTable = Array.from(table.querySelectorAll(Name)).map( 433 (resourceName: any) => resourceName.textContent 434 ) 435 436 actualResourcesFromTable[groupName] = resourcesInTable 437 }) 438 439 expect(actualResourcesFromTable).toEqual(expectedResourcesFromLabelGroups) 440 }) 441 }) 442 443 describe("resource status summary", () => { 444 it("renders summaries for each label group", () => { 445 const summaries = container.querySelectorAll(ResourceStatusSummaryRoot) 446 const totalLabelCount = 447 resources.labels.length + 448 (resources.tiltfile.length ? 1 : 0) + 449 (resources.unlabeled.length ? 1 : 0) 450 451 expect(summaries.length).toBe(totalLabelCount) 452 }) 453 }) 454 455 describe("expand and collapse", () => { 456 let groups: NodeListOf<Element> 457 458 // Helpers 459 const getResourceGroups = () => container.querySelectorAll(OverviewGroup) 460 461 beforeEach(() => { 462 groups = getResourceGroups() 463 }) 464 465 it("displays as expanded or collapsed based on the ResourceGroupContext", () => { 466 // Create an existing randomized group state from the labels 467 const { labels } = resources 468 const testData: GroupsState = [ 469 ...labels, 470 UNLABELED_LABEL, 471 TILTFILE_LABEL, 472 ].reduce((groupsState: GroupsState, label) => { 473 const randomLabelState = Math.random() > 0.5 474 groupsState[label] = { 475 ...DEFAULT_GROUP_STATE, 476 expanded: randomLabelState, 477 } 478 479 return groupsState 480 }, {}) 481 // Re-mount the component with the initial groups context values 482 container = renderContainer( 483 <MemoryRouter initialEntries={["/"]}> 484 <ResourceGroupsContextProvider initialValuesForTesting={testData}> 485 <ResourceSelectionProvider> 486 <TableGroupedByLabels 487 resources={view.uiResources} 488 buttons={view.uiButtons} 489 /> 490 </ResourceSelectionProvider> 491 </ResourceGroupsContextProvider> 492 </MemoryRouter> 493 ) 494 495 // Loop through each resource group and expect that its expanded state 496 // matches with the hardcoded test data 497 const actualExpandedState: GroupsState = {} 498 container.querySelectorAll(OverviewGroup).forEach((group: any) => { 499 const groupName = group.querySelector(OverviewGroupName).textContent 500 actualExpandedState[groupName] = { 501 expanded: group.classList.contains("Mui-expanded"), 502 } 503 }) 504 505 expect(actualExpandedState).toEqual(testData) 506 }) 507 508 it("is collapsed when an expanded resource group summary is clicked on", () => { 509 const group = groups[0] 510 expect(group.classList.contains("Mui-expanded")).toBe(true) 511 512 userEvent.click(group.querySelector('[role="button"]') as Element) 513 514 // Manually refresh the test component tree 515 groups = getResourceGroups() 516 517 const updatedGroup = groups[0] 518 expect(updatedGroup.classList.contains("Mui-expanded")).toBe(false) 519 }) 520 521 it("is expanded when a collapsed resource group summary is clicked on", () => { 522 // Because groups are expanded by default, click on it once to get it 523 // into a collapsed state for testing 524 const initialGroup = groups[0] 525 expect(initialGroup.classList.contains("Mui-expanded")).toBe(true) 526 527 userEvent.click(initialGroup.querySelector('[role="button"]') as Element) 528 529 const group = getResourceGroups()[0] 530 expect(group.classList.contains("Mui-expanded")).toBe(false) 531 532 userEvent.click(group.querySelector('[role="button"]')!) 533 534 const updatedGroup = getResourceGroups()[0] 535 expect(updatedGroup.classList.contains("Mui-expanded")).toBe(true) 536 }) 537 }) 538 539 describe("sorting", () => { 540 let firstTableNameColumn: any 541 542 beforeEach(() => { 543 // Find and click the "Resource Name" column on the first table group 544 firstTableNameColumn = screen.getAllByTitle("Sort by Resource Name")[0] 545 userEvent.click(firstTableNameColumn) 546 }) 547 548 it("tables sort by ascending values when clicked once", () => { 549 // Use the fourth resource group table, since it has multiple resources in the test data generator 550 const ascendingNames = Array.from( 551 container.querySelectorAll("table")[3].querySelectorAll(Name) 552 ) 553 const expectedNames = ["_1", "_3", "_5", "_7", "a_failed_build"] 554 const actualNames = ascendingNames.map((name: any) => name.textContent) 555 556 expect(actualNames).toStrictEqual(expectedNames) 557 }) 558 559 it("tables sort by descending values when clicked twice", () => { 560 userEvent.click(firstTableNameColumn) 561 562 // Use the fourth resource group table, since it has multiple resources in the test data generator 563 const descendingNames = Array.from( 564 container.querySelectorAll("table")[3].querySelectorAll(Name) 565 ) 566 const expectedNames = ["a_failed_build", "_7", "_5", "_3", "_1"] 567 const actualNames = descendingNames.map((name: any) => name.textContent) 568 569 expect(actualNames).toStrictEqual(expectedNames) 570 }) 571 572 it("tables un-sort when clicked thrice", () => { 573 userEvent.click(firstTableNameColumn) 574 userEvent.click(firstTableNameColumn) 575 576 // Use the fourth resource group table, since it has multiple resources in the test data generator 577 const unsortedNames = Array.from( 578 container.querySelectorAll("table")[3].querySelectorAll(Name) 579 ) 580 const expectedNames = ["_1", "_3", "_5", "_7", "a_failed_build"] 581 const actualNames = unsortedNames.map((name: any) => name.textContent) 582 583 expect(actualNames).toStrictEqual(expectedNames) 584 }) 585 }) 586 587 describe("resource name filter", () => { 588 it("does not display tables in groups when a resource filter is applied", () => { 589 const nameFilterContainer = renderContainer( 590 tableViewWithSettings({ 591 view, 592 labelsEnabled: true, 593 resourceListOptions: { 594 resourceNameFilter: "filtering!", 595 alertsOnTop: false, 596 showDisabledResources: true, 597 }, 598 }) 599 ) 600 601 expect( 602 nameFilterContainer.querySelectorAll(OverviewGroupName).length 603 ).toBe(0) 604 }) 605 }) 606 }) 607 608 describe("when disable resources feature is enabled and `showDisabledResources` option is true", () => { 609 let view: TestDataView 610 let container: HTMLElement 611 612 beforeEach(() => { 613 view = nResourceView(4) 614 // Add two disabled resources to view and place them throughout list 615 const firstDisabledResource = oneResource({ 616 name: "zee_disabled_resource", 617 disabled: true, 618 }) 619 const secondDisabledResource = oneResource({ 620 name: "_0_disabled_resource", 621 disabled: true, 622 }) 623 view.uiResources.unshift(firstDisabledResource) 624 view.uiResources.push(secondDisabledResource) 625 // Add a button to the first disabled resource 626 view.uiButtons = [ 627 oneUIButton({ componentID: firstDisabledResource.metadata!.name }), 628 ] 629 container = renderContainer( 630 tableViewWithSettings({ 631 view, 632 resourceListOptions: { 633 ...DEFAULT_OPTIONS, 634 showDisabledResources: true, 635 }, 636 }) 637 ) 638 }) 639 640 it("displays disabled resources at the bottom of the table", () => { 641 const visibleResources = Array.from(container.querySelectorAll(Name)) 642 const resourceNamesInOrder = visibleResources.map((r: any) => r.textContent) 643 expect(resourceNamesInOrder.length).toBe(6) 644 645 const expectedNameOrder = [ 646 "(Tiltfile)", 647 "_1", 648 "_2", 649 "_3", 650 "zee_disabled_resource", 651 "_0_disabled_resource", 652 ] 653 654 expect(resourceNamesInOrder).toStrictEqual(expectedNameOrder) 655 }) 656 657 it("sorts disabled resources along with enabled resources", () => { 658 // Click twice to sort by resource name descending (Z -> A) 659 let header = findTableHeaderByName("Resource Name", true) 660 userEvent.click(header) 661 userEvent.click(header) 662 663 const resourceNamesInOrder = Array.from( 664 container.querySelectorAll(Name) 665 ).map((r: any) => r.textContent) 666 667 const expectedNameOrder = [ 668 "zee_disabled_resource", 669 "_3", 670 "_2", 671 "_1", 672 "_0_disabled_resource", 673 "(Tiltfile)", 674 ] 675 676 expect(resourceNamesInOrder).toStrictEqual(expectedNameOrder) 677 }) 678 679 it("does NOT display controls for a disabled resource", () => { 680 // Get the last resource table row, which should be a disabled resource 681 const disabledResource = container.querySelectorAll(ResourceTableRow)[5] 682 const resourceName = disabledResource.querySelector(Name) 683 684 let buttons = Array.from(disabledResource.querySelectorAll("button")) 685 // Remove disabled buttons 686 .filter((button: any) => !button.classList.contains("is-disabled")) 687 .filter((button: any) => !button.classList.contains("isDisabled")) 688 // Remove the star button 689 .filter((button: any) => button.title != "Star this Resource") 690 expect(resourceName!.textContent).toBe("zee_disabled_resource") 691 expect(buttons).toHaveLength(0) 692 }) 693 694 it("adds `isDisabled` class to table rows for disabled resources", () => { 695 // Expect two disabled resources based on hardcoded test data 696 const disabledRows = container.querySelectorAll( 697 ResourceTableRow + ".isDisabled" 698 ) 699 expect(disabledRows.length).toBe(2) 700 }) 701 }) 702 703 describe("`showDisabledResources` option is false", () => { 704 it("does NOT display disabled resources", () => { 705 const view = nResourceView(8) 706 // Add a disabled resource to view 707 const disabledResource = oneResource({ 708 name: "disabled_resource", 709 disabled: true, 710 }) 711 view.uiResources.push(disabledResource) 712 713 const { container } = render( 714 tableViewWithSettings({ 715 view, 716 resourceListOptions: { 717 ...DEFAULT_OPTIONS, 718 showDisabledResources: false, 719 }, 720 }) 721 ) 722 723 const visibleResources = Array.from(container.querySelectorAll(Name)) 724 const resourceNames = visibleResources.map((r) => r.textContent) 725 expect(resourceNames.length).toBe(8) 726 expect(resourceNames).not.toContain("disabled_resource") 727 }) 728 }) 729 730 describe("bulk disable actions", () => { 731 function allEnabledCheckboxes(el: HTMLElement) { 732 return Array.from( 733 el.querySelectorAll(`${SelectionCheckbox}:not(.Mui-disabled)`) 734 ) 735 } 736 737 describe("when disable resources feature is enabled", () => { 738 let view: TestDataView 739 let container: HTMLElement 740 741 beforeEach(() => { 742 view = nResourceView(4) 743 container = renderContainer(tableViewWithSettings({ view })) 744 }) 745 746 it("renders labels on enabled checkbox", () => { 747 let els = container.querySelectorAll( 748 `${SelectionCheckbox}:not(.Mui-disabled)` 749 ) 750 expect(els[0].getAttribute("aria-label")).toBe("Resource group selection") 751 expect(els[1].getAttribute("aria-label")).toBe("Select resource") 752 }) 753 754 it("renders labels on disabled checkbox", () => { 755 let el = container.querySelector(`${SelectionCheckbox}.Mui-disabled`) 756 expect(el!.getAttribute("aria-label")).toBe("Cannot select resource") 757 }) 758 759 it("renders the `Select` column", () => { 760 expect( 761 container.querySelectorAll(SelectionCheckbox).length 762 ).toBeGreaterThan(0) 763 }) 764 765 it("renders a checkbox for the column header and every resource that is selectable", () => { 766 const expectedCheckBoxDisplay = { 767 "(Tiltfile)": false, 768 columnHeader: true, 769 _1: true, 770 _2: true, 771 _3: true, 772 } 773 const actualCheckboxDisplay: { [key: string]: boolean } = {} 774 const rows = Array.from(container.querySelectorAll(ResourceTableRow)) 775 rows.forEach((row: any, idx: number) => { 776 let name: string 777 if (idx === 0) { 778 name = "columnHeader" 779 } else { 780 name = row.querySelector(Name).textContent 781 } 782 783 const checkbox = allEnabledCheckboxes(row) 784 actualCheckboxDisplay[name] = checkbox.length === 1 785 }) 786 787 expect(actualCheckboxDisplay).toEqual(expectedCheckBoxDisplay) 788 }) 789 790 it("selects a resource when checkbox is not checked", () => { 791 const checkbox = allEnabledCheckboxes(container)[1] 792 userEvent.click(checkbox.querySelector("input")!) 793 794 const checkboxAfterClick = allEnabledCheckboxes(container)[1] 795 expect(checkboxAfterClick.getAttribute("aria-checked")).toBe("true") 796 }) 797 798 it("deselects a resource when checkbox is checked", () => { 799 const checkbox = allEnabledCheckboxes(container)[1] 800 expect(checkbox).toBeTruthy() 801 802 // Click the checkbox once to get it to a selected state 803 userEvent.click(checkbox.querySelector("input")!) 804 805 const checkboxAfterFirstClick = allEnabledCheckboxes(container)[1] 806 expect(checkboxAfterFirstClick.getAttribute("aria-checked")).toBe("true") 807 808 // Click the checkbox a second time to deselect it 809 userEvent.click(checkbox.querySelector("input")!) 810 811 const checkboxAfterSecondClick = allEnabledCheckboxes(container)[1] 812 expect(checkboxAfterSecondClick.getAttribute("aria-checked")).toBe( 813 "false" 814 ) 815 }) 816 817 describe("selection checkbox header", () => { 818 it("displays as unchecked if no resources in the table are checked", () => { 819 const allCheckboxes = allEnabledCheckboxes(container) 820 let checkbox: any = allCheckboxes[0] 821 const headerCheckboxCheckedState = checkbox.getAttribute("aria-checked") 822 const headerCheckboxIndeterminateState = checkbox 823 .querySelector("[data-indeterminate]") 824 .getAttribute("data-indeterminate") 825 const rowCheckboxesState = allCheckboxes 826 .slice(1) 827 .map((checkbox: any) => checkbox.getAttribute("aria-checked")) 828 829 expect(rowCheckboxesState).toStrictEqual(["false", "false", "false"]) 830 expect(headerCheckboxCheckedState).toBe("false") 831 expect(headerCheckboxIndeterminateState).toBe("false") 832 }) 833 834 it("displays as indeterminate if some but not all resources in the table are checked", () => { 835 // Choose a (random) table row to click and select 836 const resourceCheckbox: any = allEnabledCheckboxes(container)[2] 837 userEvent.click(resourceCheckbox.querySelector("input")) 838 839 // Verify that the header checkbox displays as partially selected 840 const headerCheckboxCheckedState = container 841 .querySelector(SelectionCheckbox)! 842 .getAttribute("aria-checked") 843 const headerCheckboxIndeterminateState = container 844 .querySelector(`${SelectionCheckbox} [data-indeterminate]`)! 845 .getAttribute("data-indeterminate") 846 847 expect(headerCheckboxCheckedState).toBe("false") 848 expect(headerCheckboxIndeterminateState).toBe("true") 849 }) 850 851 it("displays as checked if all resources in the table are checked", () => { 852 // Click all checkboxes for resource rows, skipping the first one (which is the table header row) 853 allEnabledCheckboxes(container) 854 .slice(1) 855 .forEach((resourceCheckbox: any) => { 856 userEvent.click(resourceCheckbox.querySelector("input")) 857 }) 858 859 // Verify that the header checkbox displays as partially selected 860 const headerCheckboxCheckedState = container 861 .querySelector(SelectionCheckbox)! 862 .getAttribute("aria-checked") 863 const headerCheckboxIndeterminateState = container 864 .querySelector(`${SelectionCheckbox} [data-indeterminate]`)! 865 .getAttribute("data-indeterminate") 866 867 expect(headerCheckboxCheckedState).toBe("true") 868 expect(headerCheckboxIndeterminateState).toBe("false") 869 }) 870 871 it("selects every resource in the table when checkbox is not checked", () => { 872 // Click the header checkbox to select it 873 const headerCheckbox = allEnabledCheckboxes(container)[0] 874 userEvent.click(headerCheckbox.querySelector("input")!) 875 876 // Verify all table resources are now selected 877 const rowCheckboxesState = allEnabledCheckboxes(container) 878 .slice(1) 879 .map((checkbox: any) => checkbox.getAttribute("aria-checked")) 880 expect(rowCheckboxesState).toStrictEqual(["true", "true", "true"]) 881 }) 882 883 it("deselects every resource in the table when checkbox is checked", () => { 884 const headerCheckbox = container.querySelector(SelectionCheckbox)! 885 userEvent.click(headerCheckbox.querySelector("input")!) 886 887 // Click the checkbox a second time to deselect it 888 const headerCheckboxAfterFirstClick = 889 container.querySelector(SelectionCheckbox)! 890 userEvent.click(headerCheckboxAfterFirstClick.querySelector("input")!) 891 892 // Verify all table resources are now deselected 893 const rowCheckboxesState = allEnabledCheckboxes(container) 894 .slice(1) 895 .map((checkbox: any) => checkbox.getAttribute("aria-checked")) 896 expect(rowCheckboxesState).toStrictEqual(["false", "false", "false"]) 897 }) 898 }) 899 }) 900 }) 901 902 // https://github.com/tilt-dev/tilt/issues/5754 903 it("renders the trigger mode column correctly", () => { 904 const view = nResourceView(2) 905 view.uiResources = [ 906 oneResource({ name: "r1", triggerMode: TriggerMode.TriggerModeAuto }), 907 oneResource({ name: "r2", triggerMode: TriggerMode.TriggerModeManual }), 908 ] 909 const container = renderContainer(tableViewWithSettings({ view: view })) 910 911 const isToggleContent = (content: string) => 912 content == ToggleTriggerModeTooltip.isAuto || 913 content == ToggleTriggerModeTooltip.isManual 914 915 let modes = Array.from(screen.getAllByTitle(isToggleContent)).map( 916 (n) => n.title 917 ) 918 expect(modes).toEqual([ 919 ToggleTriggerModeTooltip.isAuto, 920 ToggleTriggerModeTooltip.isManual, 921 ]) 922 }) 923 924 function renderContainer(x: ReactElement) { 925 let { container } = render(x) 926 return container 927 }