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