github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/web/src/BulkApiButton.test.tsx (about) 1 import { render, screen, waitFor } from "@testing-library/react" 2 import userEvent from "@testing-library/user-event" 3 import { AnalyticsAction, AnalyticsType } from "./analytics" 4 import { 5 cleanupMockAnalyticsCalls, 6 expectIncrs, 7 mockAnalyticsCalls, 8 nonAnalyticsCalls, 9 } from "./analytics_test_helpers" 10 import { ApiButtonToggleState, ApiButtonType } from "./ApiButton" 11 import { 12 getUIButtonDataFromCall, 13 mockUIButtonUpdates, 14 } from "./ApiButton.testhelpers" 15 import { 16 BulkApiButton, 17 canBulkButtonBeToggled, 18 canButtonBeToggled, 19 } from "./BulkApiButton" 20 import { BulkAction } from "./OverviewTableBulkActions" 21 import { flushPromises } from "./promise" 22 import { disableButton, oneUIButton } from "./testdata" 23 24 const NON_TOGGLE_BUTTON = oneUIButton({ componentID: "database" }) 25 const DISABLE_BUTTON_DB = disableButton("database", true) 26 const DISABLE_BUTTON_FRONTEND = disableButton("frontend", true) 27 const ENABLE_BUTTON_BACKEND = disableButton("backend", false) 28 const TEST_UIBUTTONS = [ 29 DISABLE_BUTTON_DB, 30 DISABLE_BUTTON_FRONTEND, 31 ENABLE_BUTTON_BACKEND, 32 ] 33 34 describe("BulkApiButton", () => { 35 beforeEach(() => { 36 mockAnalyticsCalls() 37 mockUIButtonUpdates() 38 }) 39 40 afterEach(() => { 41 cleanupMockAnalyticsCalls() 42 }) 43 44 it("is disabled when there are no UIButtons", () => { 45 render( 46 <BulkApiButton 47 bulkAction={BulkAction.Disable} 48 buttonText="I cannot be clicked" 49 requiresConfirmation={false} 50 uiButtons={[]} 51 /> 52 ) 53 54 const bulkButton = screen.getByLabelText("Trigger I cannot be clicked") 55 56 expect(bulkButton).toBeTruthy() 57 expect((bulkButton as HTMLButtonElement).disabled).toBe(true) 58 }) 59 60 it("is disabled when there are no UIButtons that can be toggled to the target toggle state", () => { 61 render( 62 <BulkApiButton 63 bulkAction={BulkAction.Disable} 64 buttonText="I cannot be toggled that way" 65 requiresConfirmation={false} 66 targetToggleState={ApiButtonToggleState.On} 67 uiButtons={[DISABLE_BUTTON_DB]} 68 /> 69 ) 70 71 const bulkButton = screen.getByLabelText( 72 "Trigger I cannot be toggled that way" 73 ) 74 75 expect(bulkButton).toBeTruthy() 76 expect((bulkButton as HTMLButtonElement).disabled).toBe(true) 77 }) 78 79 it("is enabled when there are UIButtons", () => { 80 render( 81 <BulkApiButton 82 bulkAction={BulkAction.Disable} 83 buttonText="Run lint" 84 requiresConfirmation={false} 85 uiButtons={[NON_TOGGLE_BUTTON]} 86 /> 87 ) 88 89 const bulkButton = screen.getByLabelText("Trigger Run lint") 90 91 expect(bulkButton).toBeTruthy() 92 expect((bulkButton as HTMLButtonElement).disabled).toBe(false) 93 }) 94 95 it("is enabled when there are UIButtons that can be toggled to the target toggle state", () => { 96 render( 97 <BulkApiButton 98 bulkAction={BulkAction.Disable} 99 buttonText="Enable resources" 100 requiresConfirmation={false} 101 targetToggleState={ApiButtonToggleState.On} 102 uiButtons={[DISABLE_BUTTON_DB, ENABLE_BUTTON_BACKEND]} 103 /> 104 ) 105 106 const bulkButton = screen.getByLabelText("Trigger Enable resources") 107 108 expect(bulkButton).toBeTruthy() 109 expect((bulkButton as HTMLButtonElement).disabled).toBe(false) 110 }) 111 112 describe("when it's clicked", () => { 113 let mockCallback: jest.Mock 114 115 beforeEach(async () => { 116 mockCallback = jest.fn() 117 render( 118 <BulkApiButton 119 bulkAction={BulkAction.Disable} 120 buttonText="Turn everything off" 121 onClickCallback={mockCallback} 122 requiresConfirmation={false} 123 targetToggleState={ApiButtonToggleState.Off} 124 uiButtons={TEST_UIBUTTONS} 125 /> 126 ) 127 128 const bulkButton = screen.getByLabelText("Trigger Turn everything off") 129 userEvent.click(bulkButton) 130 131 // Wait for the async calls to complete 132 await waitFor(flushPromises) 133 }) 134 135 it("triggers all buttons that can be toggled when it's clicked", () => { 136 const buttonUpdateCalls = nonAnalyticsCalls() 137 138 // Out of the three test buttons, only two of them can be toggled to the target toggle state 139 expect(buttonUpdateCalls.length).toBe(2) 140 141 const buttonUpdateNames = buttonUpdateCalls.map( 142 (call) => getUIButtonDataFromCall(call)?.metadata?.name 143 ) 144 145 expect(buttonUpdateNames).toStrictEqual([ 146 DISABLE_BUTTON_DB.metadata?.name, 147 DISABLE_BUTTON_FRONTEND.metadata?.name, 148 ]) 149 }) 150 151 it("generates the correct analytics payload", () => { 152 expectIncrs({ 153 name: "ui.web.bulkButton", 154 tags: { 155 action: AnalyticsAction.Click, 156 bulkAction: BulkAction.Disable, 157 bulkCount: "3", 158 toggleValue: ApiButtonToggleState.On, 159 component: ApiButtonType.Global, 160 type: AnalyticsType.Grid, 161 }, 162 }) 163 }) 164 165 it("calls a specified onClick callback", () => { 166 expect(mockCallback).toHaveBeenCalledTimes(1) 167 }) 168 }) 169 170 describe("when it requires confirmation", () => { 171 beforeEach(async () => { 172 render( 173 <BulkApiButton 174 bulkAction={BulkAction.Disable} 175 buttonText="Click everything when I'm sure" 176 requiresConfirmation={true} 177 uiButtons={TEST_UIBUTTONS} 178 /> 179 ) 180 181 const bulkButton = screen.getByLabelText( 182 "Trigger Click everything when I'm sure" 183 ) 184 userEvent.click(bulkButton) 185 }) 186 187 it("displays confirm and cancel buttons when clicked once", () => { 188 expect( 189 screen.getByLabelText("Confirm Click everything when I'm sure") 190 ).toBeTruthy() 191 expect( 192 screen.getByLabelText("Cancel Click everything when I'm sure") 193 ).toBeTruthy() 194 }) 195 196 it("triggers all buttons when `Confirm` is clicked", async () => { 197 userEvent.click( 198 screen.getByLabelText("Confirm Click everything when I'm sure") 199 ) 200 201 await waitFor(flushPromises) 202 203 const buttonUpdateCalls = nonAnalyticsCalls() 204 expect(buttonUpdateCalls.length).toBe(3) 205 206 const buttonUpdateNames = buttonUpdateCalls.map( 207 (call) => getUIButtonDataFromCall(call)?.metadata?.name 208 ) 209 210 expect(buttonUpdateNames).toStrictEqual([ 211 DISABLE_BUTTON_DB.metadata?.name, 212 DISABLE_BUTTON_FRONTEND.metadata?.name, 213 ENABLE_BUTTON_BACKEND.metadata?.name, 214 ]) 215 }) 216 217 it("does NOT trigger any buttons when `Cancel` is clicked", async () => { 218 userEvent.click( 219 screen.getByLabelText("Cancel Click everything when I'm sure") 220 ) 221 222 // There shouldn't be any async calls made when canceling, but wait in case 223 await waitFor(flushPromises) 224 225 expect( 226 screen.getByLabelText("Trigger Click everything when I'm sure") 227 ).toBeTruthy() 228 }) 229 230 it("generates the correct analytics payload for confirmation", async () => { 231 userEvent.click( 232 screen.getByLabelText("Confirm Click everything when I'm sure") 233 ) 234 await waitFor(flushPromises) 235 236 expectIncrs( 237 { 238 name: "ui.web.bulkButton", 239 tags: { 240 action: AnalyticsAction.Click, 241 bulkAction: BulkAction.Disable, 242 bulkCount: "3", 243 component: ApiButtonType.Global, 244 type: AnalyticsType.Grid, 245 }, 246 }, 247 { 248 name: "ui.web.bulkButton", 249 tags: { 250 action: AnalyticsAction.Click, 251 bulkAction: BulkAction.Disable, 252 bulkCount: "3", 253 confirm: "true", 254 component: ApiButtonType.Global, 255 type: AnalyticsType.Grid, 256 }, 257 } 258 ) 259 }) 260 261 it("generates the correct analytics payload for cancelation", () => { 262 userEvent.click( 263 screen.getByLabelText("Cancel Click everything when I'm sure") 264 ) 265 266 expectIncrs( 267 { 268 name: "ui.web.bulkButton", 269 tags: { 270 action: AnalyticsAction.Click, 271 bulkAction: BulkAction.Disable, 272 bulkCount: "3", 273 component: ApiButtonType.Global, 274 type: AnalyticsType.Grid, 275 }, 276 }, 277 { 278 name: "ui.web.bulkButton", 279 tags: { 280 action: AnalyticsAction.Click, 281 bulkAction: BulkAction.Disable, 282 bulkCount: "3", 283 confirm: "false", 284 component: ApiButtonType.Global, 285 type: AnalyticsType.Grid, 286 }, 287 } 288 ) 289 }) 290 }) 291 292 describe("helpers", () => { 293 describe("canButtonBeToggled", () => { 294 it("returns true when there is no target toggle state", () => { 295 expect(canButtonBeToggled(DISABLE_BUTTON_FRONTEND)).toBe(true) 296 }) 297 298 it("returns false when button is not a toggle button", () => { 299 expect(canButtonBeToggled(NON_TOGGLE_BUTTON)).toBe(false) 300 }) 301 302 it("returns false when button is already in the target toggle state", () => { 303 expect( 304 canButtonBeToggled(DISABLE_BUTTON_FRONTEND, ApiButtonToggleState.On) 305 ).toBe(false) 306 expect( 307 canButtonBeToggled(ENABLE_BUTTON_BACKEND, ApiButtonToggleState.Off) 308 ).toBe(false) 309 }) 310 311 it("returns true when button is not in the target toggle state", () => { 312 expect( 313 canButtonBeToggled(DISABLE_BUTTON_FRONTEND, ApiButtonToggleState.Off) 314 ).toBe(true) 315 expect( 316 canButtonBeToggled(ENABLE_BUTTON_BACKEND, ApiButtonToggleState.On) 317 ).toBe(true) 318 }) 319 }) 320 321 describe("canBulkButtonBeToggled", () => { 322 it("returns false if no buttons in the list of buttons can be toggled", () => { 323 expect( 324 canBulkButtonBeToggled( 325 [NON_TOGGLE_BUTTON, DISABLE_BUTTON_FRONTEND], 326 ApiButtonToggleState.On 327 ) 328 ).toBe(false) 329 }) 330 331 it("returns true if at least one button in the list of buttons can be toggled", () => { 332 expect( 333 canBulkButtonBeToggled( 334 [NON_TOGGLE_BUTTON, DISABLE_BUTTON_FRONTEND, ENABLE_BUTTON_BACKEND], 335 ApiButtonToggleState.On 336 ) 337 ).toBe(true) 338 }) 339 }) 340 }) 341 })