github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/web/src/OverviewActionBar.test.tsx (about) 1 import { render, RenderOptions, screen, within } from "@testing-library/react" 2 import userEvent from "@testing-library/user-event" 3 import { createMemoryHistory, MemoryHistory } from "history" 4 import { SnackbarProvider } from "notistack" 5 import React from "react" 6 import { Router } from "react-router" 7 import { AnalyticsAction } from "./analytics" 8 import { 9 cleanupMockAnalyticsCalls, 10 expectIncrs, 11 mockAnalyticsCalls, 12 } from "./analytics_test_helpers" 13 import { ButtonSet } from "./ApiButton" 14 import { EMPTY_FILTER_TERM, FilterLevel, FilterSource } from "./logfilters" 15 import OverviewActionBar, { 16 createLogSearch, 17 FILTER_INPUT_DEBOUNCE, 18 } from "./OverviewActionBar" 19 import { EmptyBar, FullBar } from "./OverviewActionBar.stories" 20 import { disableButton, oneResource, oneUIButton } from "./testdata" 21 22 const DEFAULT_FILTER_SET = { 23 level: FilterLevel.all, 24 source: FilterSource.all, 25 term: EMPTY_FILTER_TERM, 26 } 27 28 function customRender( 29 component: JSX.Element, 30 wrapperProps: { history: MemoryHistory }, 31 options?: RenderOptions 32 ) { 33 return render(component, { 34 wrapper: ({ children }) => ( 35 <Router history={wrapperProps.history}> 36 <SnackbarProvider>{children}</SnackbarProvider> 37 </Router> 38 ), 39 ...options, 40 }) 41 } 42 43 describe("OverviewActionBar", () => { 44 let history: MemoryHistory 45 beforeEach(() => { 46 cleanupMockAnalyticsCalls() 47 mockAnalyticsCalls() 48 history = createMemoryHistory({ initialEntries: ["/"] }) 49 }) 50 51 afterEach(() => { 52 cleanupMockAnalyticsCalls() 53 jest.useRealTimers() 54 }) 55 56 it("renders the top row with endpoints", () => { 57 customRender(<FullBar />, { history }) 58 59 expect( 60 screen.getByLabelText(/links and custom buttons/i) 61 ).toBeInTheDocument() 62 expect(screen.getAllByRole("link")).toHaveLength(2) 63 }) 64 65 it("renders the top row with pod ID", () => { 66 customRender(<FullBar />, { history }) 67 68 expect( 69 screen.getByLabelText(/links and custom buttons/i) 70 ).toBeInTheDocument() 71 expect(screen.getAllByRole("button", { name: /Pod ID/i })).toHaveLength(1) 72 }) 73 74 it("does NOT render the top row when there are no endpoints, pods, or buttons", () => { 75 customRender(<EmptyBar />, { history }) 76 77 expect(screen.queryByLabelText(/links and custom buttons/i)).toBeNull() 78 }) 79 80 describe("log filters", () => { 81 beforeEach(() => customRender(<FullBar />, { history })) 82 83 it("navigates to warning filter when warning log filter button is clicked", () => { 84 userEvent.click(screen.getByRole("button", { name: /warnings/i })) 85 86 expect(history.location.search).toEqual("?level=warn") 87 88 expectIncrs({ 89 name: "ui.web.filterLevel", 90 tags: { action: AnalyticsAction.Click, level: "warn", source: "" }, 91 }) 92 }) 93 94 it("navigates to build warning filter when both building and warning log filter buttons are clicked", () => { 95 userEvent.click( 96 screen.getByRole("button", { name: "Select warn log sources" }) 97 ) 98 userEvent.click(screen.getByRole("menuitem", { name: /build/i })) 99 100 expect(history.location.search).toEqual("?level=warn&source=build") 101 102 expectIncrs({ 103 name: "ui.web.filterSourceMenu", 104 tags: { action: AnalyticsAction.Click }, 105 }) 106 }) 107 }) 108 109 describe("disabled resource view", () => { 110 beforeEach(() => { 111 const resource = oneResource({ name: "not-enabled", disabled: true }) 112 const buttonSet: ButtonSet = { 113 default: [ 114 oneUIButton({ componentID: "not-enabled", buttonText: "Click me" }), 115 ], 116 toggleDisable: disableButton("not-enabled", false), 117 } 118 customRender( 119 <OverviewActionBar 120 resource={resource} 121 filterSet={DEFAULT_FILTER_SET} 122 buttons={buttonSet} 123 />, 124 { history } 125 ) 126 }) 127 128 it("should display the disable toggle button", () => { 129 expect( 130 screen.getByRole("button", { name: /trigger enable resource/i }) 131 ).toBeInTheDocument() 132 }) 133 134 it("should NOT display any `default` custom buttons", () => { 135 expect(screen.queryByRole("button", { name: /click me/i })).toBeNull() 136 }) 137 138 it("should NOT display the filter menu", () => { 139 expect(screen.queryByRole("button", { name: /warnings/i })).toBeNull() 140 expect(screen.queryByRole("button", { name: /errors/i })).toBeNull() 141 expect(screen.queryByRole("button", { name: /all levels/i })).toBeNull() 142 expect(screen.queryByRole("textbox")).toBeNull() 143 }) 144 145 it("should NOT display endpoint information", () => { 146 expect(screen.queryAllByRole("link")).toHaveLength(0) 147 }) 148 149 it("should NOT display podId information", () => { 150 expect(screen.queryAllByRole("button", { name: /Pod ID/i })).toHaveLength( 151 0 152 ) 153 }) 154 }) 155 156 describe("custom buttons", () => { 157 const customButtons = [ 158 oneUIButton({ componentID: "vigoda", disabled: true }), 159 ] 160 const toggleDisable = disableButton("vigoda", true) 161 beforeEach(() => { 162 customRender( 163 <OverviewActionBar 164 filterSet={DEFAULT_FILTER_SET} 165 buttons={{ default: customButtons, toggleDisable }} 166 />, 167 { history } 168 ) 169 }) 170 it("disables a button that should be disabled", () => { 171 const disabledButton = screen.getByLabelText( 172 `Trigger ${customButtons[0].spec?.text}` 173 ) 174 expect(disabledButton).toBeDisabled() 175 }) 176 177 it("renders disable-resource buttons separately from other buttons", () => { 178 const topRow = screen.getByLabelText(/links and custom buttons/i) 179 const buttonsInTopRow = within(topRow).getAllByRole("button") 180 const toggleDisableButton = screen.getByLabelText( 181 "Trigger Disable Resource" 182 ) 183 184 expect(buttonsInTopRow).toHaveLength(1) 185 expect(toggleDisableButton).toBeInTheDocument() 186 expect( 187 within(topRow).queryByLabelText("Trigger Disable Resource") 188 ).toBeNull() 189 }) 190 }) 191 192 describe("term filter input", () => { 193 it("renders with no initial value if there is no existing term filter", () => { 194 customRender(<FullBar />, { history }) 195 196 expect( 197 screen.getByRole("textbox", { name: /filter resource logs/i }) 198 ).toHaveValue("") 199 }) 200 201 it("renders with an initial value if there is an existing term filter", () => { 202 history.push({ 203 pathname: "/", 204 search: createLogSearch("", { term: "bleep bloop" }).toString(), 205 }) 206 207 customRender(<FullBar />, { history }) 208 209 expect( 210 screen.getByRole("textbox", { name: /filter resource logs/i }) 211 ).toHaveValue("bleep bloop") 212 }) 213 214 it("changes the global term filter state when its value changes", () => { 215 jest.useFakeTimers() 216 217 customRender(<FullBar />, { history }) 218 219 userEvent.type(screen.getByRole("textbox"), "docker") 220 221 jest.advanceTimersByTime(FILTER_INPUT_DEBOUNCE) 222 223 expect(history.location.search.toString()).toEqual("?term=docker") 224 }) 225 226 it("uses debouncing to update the global term filter state", () => { 227 jest.useFakeTimers() 228 229 customRender(<FullBar />, { history }) 230 231 userEvent.type(screen.getByRole("textbox"), "doc") 232 233 jest.advanceTimersByTime(FILTER_INPUT_DEBOUNCE / 2) 234 235 // The debouncing time hasn't passed yet, so we don't expect to see any changes 236 expect(history.location.search.toString()).toEqual("") 237 238 userEvent.type(screen.getByRole("textbox"), "ker") 239 240 // The debouncing time hasn't passed yet, so we don't expect to see any changes 241 expect(history.location.search.toString()).toEqual("") 242 243 jest.advanceTimersByTime(FILTER_INPUT_DEBOUNCE) 244 245 // Since the debouncing time has passed, we expect to see the final 246 // change reflected 247 expect(history.location.search.toString()).toEqual("?term=docker") 248 }) 249 250 it("retains any current level and source filters when its value changes", () => { 251 jest.useFakeTimers() 252 253 history.push({ pathname: "/", search: "level=warn&source=build" }) 254 255 customRender(<FullBar />, { history }) 256 257 userEvent.type(screen.getByRole("textbox"), "help") 258 259 jest.advanceTimersByTime(FILTER_INPUT_DEBOUNCE) 260 261 expect(history.location.search.toString()).toEqual( 262 "?level=warn&source=build&term=help" 263 ) 264 }) 265 }) 266 267 describe("createLogSearch", () => { 268 let currentSearch: URLSearchParams 269 beforeEach(() => (currentSearch = new URLSearchParams())) 270 271 it("sets the params that are passed in", () => { 272 expect( 273 createLogSearch(currentSearch.toString(), { 274 level: FilterLevel.all, 275 term: "find me", 276 source: FilterSource.build, 277 }).toString() 278 ).toBe("source=build&term=find+me") 279 280 expect( 281 createLogSearch(currentSearch.toString(), { 282 level: FilterLevel.warn, 283 }).toString() 284 ).toBe("level=warn") 285 286 expect( 287 createLogSearch(currentSearch.toString(), { 288 term: "", 289 source: FilterSource.runtime, 290 }).toString() 291 ).toBe("source=runtime") 292 }) 293 294 it("overrides params if a new value is defined", () => { 295 currentSearch.set("level", FilterLevel.warn) 296 expect( 297 createLogSearch(currentSearch.toString(), { 298 level: FilterLevel.error, 299 }).toString() 300 ).toBe("level=error") 301 currentSearch.delete("level") 302 303 currentSearch.set("level", "a meaningless value") 304 currentSearch.set("term", "") 305 expect( 306 createLogSearch(currentSearch.toString(), { 307 level: FilterLevel.all, 308 term: "service", 309 }).toString() 310 ).toBe("term=service") 311 }) 312 313 it("preserves existing params if no new value is defined", () => { 314 currentSearch.set("source", FilterSource.build) 315 expect( 316 createLogSearch(currentSearch.toString(), { 317 term: "test", 318 }).toString() 319 ).toBe("source=build&term=test") 320 }) 321 }) 322 })