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