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