github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/web/src/SidebarResources.test.tsx (about)

     1  import {
     2    render,
     3    RenderOptions,
     4    RenderResult,
     5    screen,
     6    waitFor,
     7    within,
     8  } from "@testing-library/react"
     9  import userEvent from "@testing-library/user-event"
    10  import React from "react"
    11  import { MemoryRouter } from "react-router"
    12  import { AnalyticsAction } from "./analytics"
    13  import {
    14    cleanupMockAnalyticsCalls,
    15    expectIncrs,
    16    mockAnalyticsCalls,
    17  } from "./analytics_test_helpers"
    18  import { accessorsForTesting, tiltfileKeyContext } from "./BrowserStorage"
    19  import Features, { FeaturesTestProvider, Flag } from "./feature"
    20  import LogStore from "./LogStore"
    21  import PathBuilder from "./PathBuilder"
    22  import { ResourceGroupsContextProvider } from "./ResourceGroupsContext"
    23  import {
    24    DEFAULT_OPTIONS,
    25    ResourceListOptions,
    26    ResourceListOptionsProvider,
    27    RESOURCE_LIST_OPTIONS_KEY,
    28  } from "./ResourceListOptionsContext"
    29  import SidebarItem from "./SidebarItem"
    30  import SidebarResources from "./SidebarResources"
    31  import { StarredResourcesContextProvider } from "./StarredResourcesContext"
    32  import { nResourceView, nResourceWithLabelsView, oneResource } from "./testdata"
    33  import { ResourceStatus, ResourceView } from "./types"
    34  
    35  let pathBuilder = PathBuilder.forTesting("localhost", "/")
    36  
    37  const resourceListOptionsAccessor = accessorsForTesting<ResourceListOptions>(
    38    RESOURCE_LIST_OPTIONS_KEY,
    39    sessionStorage
    40  )
    41  const starredItemsAccessor = accessorsForTesting<string[]>(
    42    "pinned-resources",
    43    localStorage
    44  )
    45  
    46  function createSidebarItems(n: number, withLabels = false) {
    47    const logStore = new LogStore()
    48    const resourceView = withLabels ? nResourceWithLabelsView : nResourceView
    49    const resources = resourceView(n).uiResources
    50    return resources.map((r) => new SidebarItem(r, logStore))
    51  }
    52  
    53  function createSidebarItemsWithAlerts() {
    54    const logStore = new LogStore()
    55    return [
    56      oneResource({ isBuilding: true }),
    57      oneResource({ name: "a" }),
    58      oneResource({ name: "b" }),
    59      oneResource({ name: "c", disabled: true }),
    60    ].map((res) => new SidebarItem(res, logStore))
    61  }
    62  
    63  function customRender(
    64    componentOptions: {
    65      items: SidebarItem[]
    66      selected?: string
    67      resourceListOptions?: ResourceListOptions
    68    },
    69    renderOptions?: RenderOptions
    70  ) {
    71    const features = new Features({
    72      [Flag.Labels]: true,
    73    })
    74    const listOptions = componentOptions.resourceListOptions ?? DEFAULT_OPTIONS
    75    return render(
    76      <SidebarResources
    77        items={componentOptions.items}
    78        selected={componentOptions.selected ?? ""}
    79        resourceView={ResourceView.Log}
    80        pathBuilder={pathBuilder}
    81        resourceListOptions={listOptions}
    82      />,
    83      {
    84        wrapper: ({ children }) => (
    85          <MemoryRouter>
    86            <tiltfileKeyContext.Provider value="test">
    87              <FeaturesTestProvider value={features}>
    88                <StarredResourcesContextProvider>
    89                  <ResourceGroupsContextProvider>
    90                    <ResourceListOptionsProvider>
    91                      {children}
    92                    </ResourceListOptionsProvider>
    93                  </ResourceGroupsContextProvider>
    94                </StarredResourcesContextProvider>
    95              </FeaturesTestProvider>
    96            </tiltfileKeyContext.Provider>
    97          </MemoryRouter>
    98        ),
    99        ...renderOptions,
   100      }
   101    )
   102  }
   103  
   104  describe("SidebarResources", () => {
   105    beforeEach(() => {
   106      mockAnalyticsCalls()
   107      sessionStorage.clear()
   108      localStorage.clear()
   109    })
   110  
   111    afterEach(() => {
   112      cleanupMockAnalyticsCalls()
   113      sessionStorage.clear()
   114      localStorage.clear()
   115    })
   116  
   117    describe("starring resources", () => {
   118      const items = createSidebarItems(2)
   119  
   120      it("adds items to the starred list when items are starred", async () => {
   121        const itemToStar = items[1].name
   122        customRender({ items: items })
   123  
   124        userEvent.click(
   125          screen.getByRole("button", { name: `Star ${itemToStar}` })
   126        )
   127  
   128        await waitFor(() => {
   129          expect(starredItemsAccessor.get()).toEqual([itemToStar])
   130        })
   131  
   132        expectIncrs(
   133          {
   134            name: "ui.web.star",
   135            tags: { starCount: "0", action: AnalyticsAction.Load },
   136          },
   137          {
   138            name: "ui.web.sidebarStarButton",
   139            tags: {
   140              action: AnalyticsAction.Click,
   141              newStarState: "true",
   142              target: "k8s",
   143            },
   144          },
   145          {
   146            name: "ui.web.star",
   147            tags: { starCount: "1", action: AnalyticsAction.Star },
   148          }
   149        )
   150      })
   151  
   152      it("removes items from the starred list when items are unstarred", async () => {
   153        starredItemsAccessor.set(items.map((i) => i.name))
   154        customRender({ items })
   155  
   156        userEvent.click(
   157          screen.getByRole("button", { name: `Unstar ${items[1].name}` })
   158        )
   159  
   160        await waitFor(() => {
   161          expect(starredItemsAccessor.get()).toEqual([items[0].name])
   162        })
   163  
   164        expectIncrs(
   165          {
   166            name: "ui.web.star",
   167            tags: { starCount: "2", action: AnalyticsAction.Load },
   168          },
   169          {
   170            name: "ui.web.sidebarStarButton",
   171            tags: {
   172              action: AnalyticsAction.Click,
   173              newStarState: "false",
   174              target: "k8s",
   175            },
   176          },
   177          {
   178            name: "ui.web.star",
   179            tags: { starCount: "1", action: AnalyticsAction.Unstar },
   180          }
   181        )
   182      })
   183    })
   184  
   185    describe("resource list options", () => {
   186      const items = createSidebarItemsWithAlerts()
   187  
   188      const loadCases: [string, ResourceListOptions, string[]][] = [
   189        [
   190          "alertsOnTop",
   191          { ...DEFAULT_OPTIONS, alertsOnTop: true },
   192          ["vigoda", "a", "b"],
   193        ],
   194        [
   195          "resourceNameFilter",
   196          { ...DEFAULT_OPTIONS, resourceNameFilter: "vig" },
   197          ["vigoda"],
   198        ],
   199        [
   200          "showDisabledResources",
   201          { ...DEFAULT_OPTIONS, showDisabledResources: true },
   202          ["vigoda", "a", "b", "c"],
   203        ],
   204      ]
   205      test.each(loadCases)(
   206        "loads %p from browser storage",
   207        (_name, resourceListOptions, expectedItems) => {
   208          resourceListOptionsAccessor.set(resourceListOptions)
   209  
   210          customRender({ items, resourceListOptions })
   211  
   212          // Find the sidebar items for the expected list
   213          expectedItems.forEach((item) => {
   214            expect(screen.getByText(item, { exact: true })).toBeInTheDocument()
   215          })
   216  
   217          // Check that each option reflects the storage value
   218          const aotToggle = screen.getByLabelText("Alerts on top")
   219          expect((aotToggle as HTMLInputElement).checked).toBe(
   220            resourceListOptions.alertsOnTop
   221          )
   222  
   223          const resourceNameFilter = screen.getByPlaceholderText(
   224            "Filter resources by name"
   225          )
   226          expect(resourceNameFilter).toHaveValue(
   227            resourceListOptions.resourceNameFilter
   228          )
   229  
   230          const disabledToggle = screen.getByLabelText("Show disabled resources")
   231          expect(disabledToggle).toBeTruthy()
   232          expect((disabledToggle as HTMLInputElement).checked).toBe(
   233            resourceListOptions.showDisabledResources
   234          )
   235        }
   236      )
   237  
   238      const saveCases: [string, ResourceListOptions][] = [
   239        ["alertsOnTop", { ...DEFAULT_OPTIONS, alertsOnTop: true }],
   240        ["resourceNameFilter", { ...DEFAULT_OPTIONS, resourceNameFilter: "foo" }],
   241        [
   242          "showDisabledResources",
   243          { ...DEFAULT_OPTIONS, showDisabledResources: true },
   244        ],
   245      ]
   246      test.each(saveCases)(
   247        "saves option %s to browser storage",
   248        (_name, expectedOptions) => {
   249          customRender({ items })
   250  
   251          const aotToggle = screen.getByLabelText("Alerts on top")
   252          if (
   253            (aotToggle as HTMLInputElement).checked !==
   254            expectedOptions.alertsOnTop
   255          ) {
   256            userEvent.click(aotToggle)
   257          }
   258  
   259          const resourceNameFilter = screen.getByPlaceholderText(
   260            "Filter resources by name"
   261          )
   262          if (expectedOptions.resourceNameFilter) {
   263            userEvent.type(resourceNameFilter, expectedOptions.resourceNameFilter)
   264          }
   265  
   266          const disabledToggle = screen.getByLabelText("Show disabled resources")
   267          if (
   268            (disabledToggle as HTMLInputElement).checked !==
   269            expectedOptions.showDisabledResources
   270          ) {
   271            userEvent.click(disabledToggle)
   272          }
   273  
   274          const observedOptions = resourceListOptionsAccessor.get()
   275          expect(observedOptions).toEqual(expectedOptions)
   276        }
   277      )
   278    })
   279  
   280    describe("disabled resources", () => {
   281      describe("when feature flag is enabled and `showDisabledResources` option is true", () => {
   282        let rerender: RenderResult["rerender"]
   283  
   284        beforeEach(() => {
   285          // Create a list of sidebar items with disable resources interspersed
   286          const items = createSidebarItems(5)
   287          items[1].runtimeStatus = ResourceStatus.Disabled
   288          items[3].runtimeStatus = ResourceStatus.Disabled
   289  
   290          rerender = customRender({
   291            items,
   292            resourceListOptions: {
   293              ...DEFAULT_OPTIONS,
   294              showDisabledResources: true,
   295            },
   296          }).rerender
   297        })
   298  
   299        it("displays disabled resources list title", () => {
   300          expect(
   301            screen.getByText("Disabled", { exact: true })
   302          ).toBeInTheDocument()
   303        })
   304  
   305        it("displays disabled resources in their own list", () => {
   306          // Get the disabled resources list and query within it
   307          const disabledResourceList = screen.getByLabelText("Disabled resources")
   308  
   309          expect(within(disabledResourceList).getByText("_1")).toBeInTheDocument()
   310          expect(within(disabledResourceList).getByText("_3")).toBeInTheDocument()
   311        })
   312  
   313        describe("when there is a resource name filter", () => {
   314          beforeEach(() => {
   315            // Create a list of sidebar items with disable resources interspersed
   316            const itemsWithFilter = createSidebarItems(11)
   317            itemsWithFilter[1].runtimeStatus = ResourceStatus.Disabled
   318            itemsWithFilter[3].runtimeStatus = ResourceStatus.Disabled
   319            itemsWithFilter[8].runtimeStatus = ResourceStatus.Disabled
   320  
   321            const options = {
   322              resourceNameFilter: "1",
   323              alertsOnTop: true,
   324              showDisabledResources: true,
   325            }
   326  
   327            rerender(
   328              <SidebarResources
   329                items={itemsWithFilter}
   330                selected=""
   331                resourceView={ResourceView.Log}
   332                pathBuilder={pathBuilder}
   333                resourceListOptions={options}
   334              />
   335            )
   336          })
   337  
   338          it("displays disabled resources that match the filter", () => {
   339            // Expect that all matching resources (enabled + disabled) are displayed
   340            expect(screen.getByText("_1", { exact: true })).toBeInTheDocument()
   341            expect(screen.getByText("_10", { exact: true })).toBeInTheDocument()
   342  
   343            // Expect that all disabled resources appear in their own section
   344            const disabledItemsList = screen.getByLabelText("Disabled resources")
   345            expect(within(disabledItemsList).getByText("_1")).toBeInTheDocument()
   346          })
   347        })
   348  
   349        describe("when there are groups and multiple groups have disabled resources", () => {
   350          it("displays disabled resources within each group", () => {
   351            const itemsWithLabels = createSidebarItems(10, true)
   352            // Add disabled items in different label groups based on hardcoded data
   353            itemsWithLabels[2].runtimeStatus = ResourceStatus.Disabled
   354            itemsWithLabels[5].runtimeStatus = ResourceStatus.Disabled
   355  
   356            rerender(
   357              <SidebarResources
   358                items={itemsWithLabels}
   359                selected=""
   360                resourceView={ResourceView.Log}
   361                pathBuilder={pathBuilder}
   362                resourceListOptions={{
   363                  ...DEFAULT_OPTIONS,
   364                  showDisabledResources: true,
   365                }}
   366              />
   367            )
   368  
   369            expect(screen.getAllByLabelText("Disabled resources")).toHaveLength(2)
   370          })
   371        })
   372      })
   373  
   374      describe("`showDisabledResources` is false", () => {
   375        it("does NOT display disabled resources at all", () => {
   376          expect(screen.queryByLabelText("Disabled resources")).toBeNull()
   377          expect(screen.queryByText("_1", { exact: true })).toBeNull()
   378          expect(screen.queryByText("_3", { exact: true })).toBeNull()
   379        })
   380  
   381        it("does NOT display disabled resources list title", () => {
   382          expect(screen.queryByText("Disabled", { exact: true })).toBeNull()
   383        })
   384  
   385        describe("when there are groups and an entire group is disabled", () => {
   386          it("does NOT display the group section", () => {
   387            const items = createSidebarItems(5, true)
   388            // Disable the resource that's in the label group with only one resource
   389            items[3].runtimeStatus = ResourceStatus.Disabled
   390  
   391            customRender({ items })
   392  
   393            // The test data has one group with only disabled resources,
   394            // so expect that it doesn't show up
   395            expect(screen.queryByText("very_long_long_long_label")).toBeNull()
   396          })
   397        })
   398      })
   399    })
   400  })