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