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 })