github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/web/src/BuildButton.test.tsx (about) 1 import { 2 fireEvent, 3 render, 4 RenderOptions, 5 screen, 6 } from "@testing-library/react" 7 import userEvent from "@testing-library/user-event" 8 import fetchMock from "fetch-mock" 9 import { SnackbarProvider } from "notistack" 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 BuildButton, { StartBuildButtonProps } from "./BuildButton" 19 import { oneUIButton } from "./testdata" 20 import { BuildButtonTooltip, startBuild } from "./trigger" 21 import { TriggerMode } from "./types" 22 23 function expectClickable(button: HTMLElement, expected: boolean) { 24 if (expected) { 25 expect(button).toHaveClass("is-clickable") 26 expect(button).not.toBeDisabled() 27 } else { 28 expect(button).not.toHaveClass("is-clickable") 29 expect(button).toBeDisabled() 30 } 31 } 32 function expectManualStartBuildIcon(expected: boolean) { 33 const iconId = expected ? "build-manual-icon" : "build-auto-icon" 34 expect(screen.getByTestId(iconId)).toBeInTheDocument() 35 } 36 function expectIsSelected(button: HTMLElement, expected: boolean) { 37 if (expected) { 38 expect(button).toHaveClass("is-selected") 39 } else { 40 expect(button).not.toHaveClass("is-selected") 41 } 42 } 43 function expectIsQueued(button: HTMLElement, expected: boolean) { 44 if (expected) { 45 expect(button).toHaveClass("is-queued") 46 } else { 47 expect(button).not.toHaveClass("is-queued") 48 } 49 } 50 function expectWithTooltip(expected: string) { 51 expect(screen.getByTitle(expected)).toBeInTheDocument() 52 } 53 54 const stopBuildButton = oneUIButton({ buttonName: "stopBuild" }) 55 56 function customRender( 57 buttonProps: Partial<StartBuildButtonProps>, 58 options?: RenderOptions 59 ) { 60 return render( 61 <BuildButton 62 stopBuildButton={stopBuildButton} 63 onStartBuild={buttonProps.onStartBuild ?? (() => {})} 64 hasBuilt={buttonProps.hasBuilt ?? false} 65 isBuilding={buttonProps.isBuilding ?? false} 66 isSelected={buttonProps.isSelected} 67 isQueued={buttonProps.isQueued ?? false} 68 hasPendingChanges={buttonProps.hasPendingChanges ?? false} 69 triggerMode={buttonProps.triggerMode ?? TriggerMode.TriggerModeAuto} 70 analyticsTags={buttonProps.analyticsTags ?? {}} 71 />, 72 { 73 wrapper: ({ children }) => ( 74 <MemoryRouter initialEntries={["/"]}> 75 <SnackbarProvider>{children}</SnackbarProvider> 76 </MemoryRouter> 77 ), 78 ...options, 79 } 80 ) 81 } 82 83 describe("SidebarBuildButton", () => { 84 beforeEach(() => { 85 mockAnalyticsCalls() 86 fetchMock.mock("/api/trigger", JSON.stringify({})) 87 }) 88 89 afterEach(() => { 90 cleanupMockAnalyticsCalls() 91 }) 92 93 describe("start builds", () => { 94 it("POSTs to endpoint when clicked", () => { 95 customRender({ 96 onStartBuild: () => startBuild("doggos"), 97 hasBuilt: true, 98 analyticsTags: { target: "k8s" }, 99 }) 100 101 const buildButton = screen.getByLabelText(BuildButtonTooltip.Default) 102 expect(buildButton).toBeInTheDocument() 103 104 // Construct a mouse event with method spies 105 const preventDefault = jest.fn() 106 const stopPropagation = jest.fn() 107 const clickEvent = new MouseEvent("click", { bubbles: true }) 108 clickEvent.preventDefault = preventDefault 109 clickEvent.stopPropagation = stopPropagation 110 111 fireEvent(buildButton, clickEvent) 112 113 expect(preventDefault).toHaveBeenCalled() 114 expect(stopPropagation).toHaveBeenCalled() 115 116 expectIncrs({ 117 name: "ui.web.triggerResource", 118 tags: { action: AnalyticsAction.Click, target: "k8s" }, 119 }) 120 121 expect(fetchMock.calls().length).toEqual(2) 122 expect(fetchMock.calls()[1][0]).toEqual("/api/trigger") 123 expect(fetchMock.calls()[1][1]?.method).toEqual("post") 124 expect(fetchMock.calls()[1][1]?.body).toEqual( 125 JSON.stringify({ 126 manifest_names: ["doggos"], 127 build_reason: 16 /* BuildReasonFlagTriggerWeb */, 128 }) 129 ) 130 }) 131 132 it("disables button when resource is queued", () => { 133 const startBuildSpy = jest.fn() 134 customRender({ isQueued: true, onStartBuild: startBuildSpy }) 135 136 const buildButton = screen.getByLabelText( 137 BuildButtonTooltip.AlreadyQueued 138 ) 139 expect(buildButton).toBeDisabled() 140 141 userEvent.click(buildButton, undefined, { skipPointerEventsCheck: true }) 142 143 expect(startBuildSpy).not.toHaveBeenCalled() 144 }) 145 146 it("shows the button for TriggerModeManual", () => { 147 const startBuildSpy = jest.fn() 148 customRender({ 149 triggerMode: TriggerMode.TriggerModeManual, 150 onStartBuild: startBuildSpy, 151 }) 152 153 expectManualStartBuildIcon(true) 154 }) 155 156 test.each([true, false])( 157 "shows clickable + bold start build button for manual resource. hasPendingChanges: %s", 158 (hasPendingChanges) => { 159 customRender({ 160 triggerMode: TriggerMode.TriggerModeManual, 161 hasPendingChanges, 162 hasBuilt: !hasPendingChanges, 163 }) 164 165 const tooltipText = hasPendingChanges 166 ? BuildButtonTooltip.NeedsManualTrigger 167 : BuildButtonTooltip.Default 168 const buildButton = screen.getByLabelText(tooltipText) 169 170 expect(buildButton).toBeInTheDocument() 171 expectClickable(buildButton, true) 172 expectManualStartBuildIcon(hasPendingChanges) 173 expectIsQueued(buildButton, false) 174 expectWithTooltip(tooltipText) 175 } 176 ) 177 178 test.each([true, false])( 179 "shows selected trigger button for resource is selected: %p", 180 (isSelected) => { 181 customRender({ isSelected, hasBuilt: true }) 182 183 const buildButton = screen.getByLabelText(BuildButtonTooltip.Default) 184 185 expect(buildButton).toBeInTheDocument() 186 expectIsSelected(buildButton, isSelected) // Selected resource 187 } 188 ) 189 190 // A pending resource may mean that a pod is being rolled out, but is not 191 // ready yet. In that case, the start build button will delete the pod (cancelling 192 // the rollout) and rebuild. 193 it("shows start build button when pending but no current build", () => { 194 customRender({ hasPendingChanges: true, hasBuilt: true }) 195 196 const buildButton = screen.getByLabelText(BuildButtonTooltip.Default) 197 198 expect(buildButton).toBeInTheDocument() 199 expectClickable(buildButton, true) 200 expectManualStartBuildIcon(false) 201 expectIsQueued(buildButton, false) 202 expectWithTooltip(BuildButtonTooltip.Default) 203 }) 204 205 it("renders an unclickable start build button if resource waiting for first build", () => { 206 customRender({}) 207 208 const buildButton = screen.getByLabelText( 209 BuildButtonTooltip.UpdateInProgOrPending 210 ) 211 212 expect(buildButton).toBeInTheDocument() 213 expectClickable(buildButton, false) 214 expectManualStartBuildIcon(false) 215 expectIsQueued(buildButton, false) 216 expectWithTooltip(BuildButtonTooltip.UpdateInProgOrPending) 217 }) 218 219 it("renders queued resource with class .isQueued and NOT .clickable", () => { 220 customRender({ isQueued: true }) 221 222 const buildButton = screen.getByLabelText( 223 BuildButtonTooltip.AlreadyQueued 224 ) 225 226 expect(buildButton).toBeInTheDocument() 227 expectClickable(buildButton, false) 228 expectManualStartBuildIcon(false) 229 expectIsQueued(buildButton, true) 230 expectWithTooltip(BuildButtonTooltip.AlreadyQueued) 231 }) 232 }) 233 234 describe("stop builds", () => { 235 it("renders a stop button when the build is in progress", () => { 236 customRender({ isBuilding: true }) 237 238 const buildButton = screen.getByLabelText( 239 `Trigger ${stopBuildButton.spec?.text}` 240 ) 241 242 expect(buildButton).toBeInTheDocument() 243 // The button group has the .stop-button class 244 expect(screen.getByRole("group")).toHaveClass("stop-button") 245 expectWithTooltip(BuildButtonTooltip.Stop) 246 }) 247 }) 248 })