starling-devex 0.1.2

Starling: a local dev orchestrator with a central daemon, shared named-URL proxy, and a k9s-style TUI (a Rust port of Tilt + portless)
import { render, screen, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { ApiButtonToggleState, ApiButtonType } from "./ApiButton"
import fetchMock from "fetch-mock"
import {
  getUIButtonDataFromCall,
  mockUIButtonUpdates,
} from "./ApiButton.testhelpers"
import {
  BulkApiButton,
  canBulkButtonBeToggled,
  canButtonBeToggled,
} from "./BulkApiButton"
import { BulkAction } from "./OverviewTableBulkActions"
import { flushPromises } from "./promise"
import { disableButton, oneUIButton } from "./testdata"

const NON_TOGGLE_BUTTON = oneUIButton({ componentID: "database" })
const DISABLE_BUTTON_DB = disableButton("database", true)
const DISABLE_BUTTON_FRONTEND = disableButton("frontend", true)
const ENABLE_BUTTON_BACKEND = disableButton("backend", false)
const TEST_UIBUTTONS = [
  DISABLE_BUTTON_DB,
  DISABLE_BUTTON_FRONTEND,
  ENABLE_BUTTON_BACKEND,
]

describe("BulkApiButton", () => {
  beforeEach(() => {
    mockUIButtonUpdates()
  })

  afterEach(() => {
    fetchMock.reset()
  })

  it("is disabled when there are no UIButtons", () => {
    render(
      <BulkApiButton
        bulkAction={BulkAction.Disable}
        buttonText="I cannot be clicked"
        requiresConfirmation={false}
        uiButtons={[]}
      />
    )

    const bulkButton = screen.getByLabelText("Trigger I cannot be clicked")

    expect(bulkButton).toBeTruthy()
    expect((bulkButton as HTMLButtonElement).disabled).toBe(true)
  })

  it("is disabled when there are no UIButtons that can be toggled to the target toggle state", () => {
    render(
      <BulkApiButton
        bulkAction={BulkAction.Disable}
        buttonText="I cannot be toggled that way"
        requiresConfirmation={false}
        targetToggleState={ApiButtonToggleState.On}
        uiButtons={[DISABLE_BUTTON_DB]}
      />
    )

    const bulkButton = screen.getByLabelText(
      "Trigger I cannot be toggled that way"
    )

    expect(bulkButton).toBeTruthy()
    expect((bulkButton as HTMLButtonElement).disabled).toBe(true)
  })

  it("is enabled when there are UIButtons", () => {
    render(
      <BulkApiButton
        bulkAction={BulkAction.Disable}
        buttonText="Run lint"
        requiresConfirmation={false}
        uiButtons={[NON_TOGGLE_BUTTON]}
      />
    )

    const bulkButton = screen.getByLabelText("Trigger Run lint")

    expect(bulkButton).toBeTruthy()
    expect((bulkButton as HTMLButtonElement).disabled).toBe(false)
  })

  it("is enabled when there are UIButtons that can be toggled to the target toggle state", () => {
    render(
      <BulkApiButton
        bulkAction={BulkAction.Disable}
        buttonText="Enable resources"
        requiresConfirmation={false}
        targetToggleState={ApiButtonToggleState.On}
        uiButtons={[DISABLE_BUTTON_DB, ENABLE_BUTTON_BACKEND]}
      />
    )

    const bulkButton = screen.getByLabelText("Trigger Enable resources")

    expect(bulkButton).toBeTruthy()
    expect((bulkButton as HTMLButtonElement).disabled).toBe(false)
  })

  describe("when it's clicked", () => {
    let mockCallback: jest.Mock

    beforeEach(async () => {
      mockCallback = jest.fn()
      render(
        <BulkApiButton
          bulkAction={BulkAction.Disable}
          buttonText="Turn everything off"
          onClickCallback={mockCallback}
          requiresConfirmation={false}
          targetToggleState={ApiButtonToggleState.Off}
          uiButtons={TEST_UIBUTTONS}
        />
      )

      const bulkButton = screen.getByLabelText("Trigger Turn everything off")
      userEvent.click(bulkButton)

      // Wait for the async calls to complete
      await waitFor(flushPromises)
    })

    it("triggers all buttons that can be toggled when it's clicked", () => {
      const buttonUpdateCalls = fetchMock.calls()

      // Out of the three test buttons, only two of them can be toggled to the target toggle state
      expect(buttonUpdateCalls.length).toBe(2)

      const buttonUpdateNames = buttonUpdateCalls.map(
        (call) => getUIButtonDataFromCall(call)?.metadata?.name
      )

      expect(buttonUpdateNames).toStrictEqual([
        DISABLE_BUTTON_DB.metadata?.name,
        DISABLE_BUTTON_FRONTEND.metadata?.name,
      ])
    })

    it("calls a specified onClick callback", () => {
      expect(mockCallback).toHaveBeenCalledTimes(1)
    })
  })

  describe("when it requires confirmation", () => {
    beforeEach(async () => {
      render(
        <BulkApiButton
          bulkAction={BulkAction.Disable}
          buttonText="Click everything when I'm sure"
          requiresConfirmation={true}
          uiButtons={TEST_UIBUTTONS}
        />
      )

      const bulkButton = screen.getByLabelText(
        "Trigger Click everything when I'm sure"
      )
      userEvent.click(bulkButton)
    })

    it("displays confirm and cancel buttons when clicked once", () => {
      expect(
        screen.getByLabelText("Confirm Click everything when I'm sure")
      ).toBeTruthy()
      expect(
        screen.getByLabelText("Cancel Click everything when I'm sure")
      ).toBeTruthy()
    })

    it("triggers all buttons when `Confirm` is clicked", async () => {
      userEvent.click(
        screen.getByLabelText("Confirm Click everything when I'm sure")
      )

      await waitFor(flushPromises)

      const buttonUpdateCalls = fetchMock.calls()
      expect(buttonUpdateCalls.length).toBe(3)

      const buttonUpdateNames = buttonUpdateCalls.map(
        (call) => getUIButtonDataFromCall(call)?.metadata?.name
      )

      expect(buttonUpdateNames).toStrictEqual([
        DISABLE_BUTTON_DB.metadata?.name,
        DISABLE_BUTTON_FRONTEND.metadata?.name,
        ENABLE_BUTTON_BACKEND.metadata?.name,
      ])
    })

    it("does NOT trigger any buttons when `Cancel` is clicked", async () => {
      userEvent.click(
        screen.getByLabelText("Cancel Click everything when I'm sure")
      )

      // There shouldn't be any async calls made when canceling, but wait in case
      await waitFor(flushPromises)

      expect(
        screen.getByLabelText("Trigger Click everything when I'm sure")
      ).toBeTruthy()
    })
  })

  describe("helpers", () => {
    describe("canButtonBeToggled", () => {
      it("returns true when there is no target toggle state", () => {
        expect(canButtonBeToggled(DISABLE_BUTTON_FRONTEND)).toBe(true)
      })

      it("returns false when button is not a toggle button", () => {
        expect(canButtonBeToggled(NON_TOGGLE_BUTTON)).toBe(false)
      })

      it("returns false when button is already in the target toggle state", () => {
        expect(
          canButtonBeToggled(DISABLE_BUTTON_FRONTEND, ApiButtonToggleState.On)
        ).toBe(false)
        expect(
          canButtonBeToggled(ENABLE_BUTTON_BACKEND, ApiButtonToggleState.Off)
        ).toBe(false)
      })

      it("returns true when button is not in the target toggle state", () => {
        expect(
          canButtonBeToggled(DISABLE_BUTTON_FRONTEND, ApiButtonToggleState.Off)
        ).toBe(true)
        expect(
          canButtonBeToggled(ENABLE_BUTTON_BACKEND, ApiButtonToggleState.On)
        ).toBe(true)
      })
    })

    describe("canBulkButtonBeToggled", () => {
      it("returns false if no buttons in the list of buttons can be toggled", () => {
        expect(
          canBulkButtonBeToggled(
            [NON_TOGGLE_BUTTON, DISABLE_BUTTON_FRONTEND],
            ApiButtonToggleState.On
          )
        ).toBe(false)
      })

      it("returns true if at least one button in the list of buttons can be toggled", () => {
        expect(
          canBulkButtonBeToggled(
            [NON_TOGGLE_BUTTON, DISABLE_BUTTON_FRONTEND, ENABLE_BUTTON_BACKEND],
            ApiButtonToggleState.On
          )
        ).toBe(true)
      })
    })
  })
})