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 {
  act,
  render,
  RenderOptions,
  screen,
  within,
} from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { MemoryRouter } from "react-router"
import { useLocation } from "react-router-dom"
import { SnackbarProvider } from "notistack"
import React from "react"
import { ButtonSet } from "./ApiButton"
import { EMPTY_FILTER_TERM, FilterLevel, FilterSource } from "./logfilters"
import OverviewActionBar, {
  createLogSearch,
  FILTER_INPUT_DEBOUNCE,
} from "./OverviewActionBar"
import { EmptyBar, FullBar } from "./OverviewActionBar.stories"
import { disableButton, oneResource, oneUIButton } from "./testdata"

const DEFAULT_FILTER_SET = {
  level: FilterLevel.all,
  source: FilterSource.all,
  term: EMPTY_FILTER_TERM,
}

let location: any = window.location

function LocationCapture() {
  location = useLocation()
  return null
}

// Helper to extract search params from the rendered MemoryRouter
function getSearch() {
  return location.search
}

function customRender(
  component: JSX.Element,
  wrapperProps: { initialEntries?: string[] } = {},
  options?: RenderOptions
) {
  return render(component, {
    wrapper: ({ children }) => (
      <MemoryRouter
        initialEntries={wrapperProps.initialEntries || ["/"]}
        future={{ v7_startTransition: true, v7_relativeSplatPath: true }}
      >
        <SnackbarProvider>{children}</SnackbarProvider>
        <LocationCapture />
      </MemoryRouter>
    ),
    ...options,
  })
}

describe("OverviewActionBar", () => {
  it("renders the top row with endpoints", () => {
    customRender(<FullBar />)

    expect(
      screen.getByLabelText(/links and custom buttons/i)
    ).toBeInTheDocument()
    expect(screen.getAllByRole("link")).toHaveLength(2)
  })

  it("renders the top row with pod ID", () => {
    customRender(<FullBar />)

    expect(
      screen.getByLabelText(/links and custom buttons/i)
    ).toBeInTheDocument()
    expect(screen.getAllByRole("button", { name: /Pod ID/i })).toHaveLength(1)
  })

  it("does NOT render the top row when there are no endpoints, pods, or buttons", () => {
    customRender(<EmptyBar />)

    expect(screen.queryByLabelText(/links and custom buttons/i)).toBeNull()
  })

  describe("log filters", () => {
    beforeEach(() => customRender(<FullBar />))

    it("navigates to warning filter when warning log filter button is clicked", () => {
      userEvent.click(screen.getByRole("button", { name: /warnings/i }))

      expect(getSearch()).toEqual("?level=warn")
    })

    it("navigates to build warning filter when both building and warning log filter buttons are clicked", () => {
      userEvent.click(
        screen.getByRole("button", { name: "Select warn log sources" })
      )
      userEvent.click(screen.getByRole("menuitem", { name: /build/i }))

      expect(getSearch()).toEqual("?level=warn&source=build")
    })
  })

  describe("disabled resource view", () => {
    beforeEach(() => {
      const resource = oneResource({ name: "not-enabled", disabled: true })
      const buttonSet: ButtonSet = {
        default: [
          oneUIButton({ componentID: "not-enabled", buttonText: "Click me" }),
        ],
        toggleDisable: disableButton("not-enabled", false),
      }
      customRender(
        <OverviewActionBar
          resource={resource}
          filterSet={DEFAULT_FILTER_SET}
          buttons={buttonSet}
        />
      )
    })

    it("should display the disable toggle button", () => {
      expect(
        screen.getByRole("button", { name: /trigger enable resource/i })
      ).toBeInTheDocument()
    })

    it("should NOT display any `default` custom buttons", () => {
      expect(screen.queryByRole("button", { name: /click me/i })).toBeNull()
    })

    it("should NOT display the filter menu", () => {
      expect(screen.queryByRole("button", { name: /warnings/i })).toBeNull()
      expect(screen.queryByRole("button", { name: /errors/i })).toBeNull()
      expect(screen.queryByRole("button", { name: /all levels/i })).toBeNull()
      expect(screen.queryByRole("textbox")).toBeNull()
    })

    it("should NOT display endpoint information", () => {
      expect(screen.queryAllByRole("link")).toHaveLength(0)
    })

    it("should NOT display podId information", () => {
      expect(screen.queryAllByRole("button", { name: /Pod ID/i })).toHaveLength(
        0
      )
    })
  })

  describe("custom buttons", () => {
    const customButtons = [
      oneUIButton({ componentID: "vigoda", disabled: true }),
    ]
    const toggleDisable = disableButton("vigoda", true)
    beforeEach(() => {
      customRender(
        <OverviewActionBar
          filterSet={DEFAULT_FILTER_SET}
          buttons={{ default: customButtons, toggleDisable }}
        />
      )
    })
    it("disables a button that should be disabled", () => {
      const disabledButton = screen.getByLabelText(
        `Trigger ${customButtons[0].spec?.text}`
      )
      expect(disabledButton).toBeDisabled()
    })

    it("renders disable-resource buttons separately from other buttons", () => {
      const topRow = screen.getByLabelText(/links and custom buttons/i)
      const buttonsInTopRow = within(topRow).getAllByRole("button")
      const toggleDisableButton = screen.getByLabelText(
        "Trigger Disable Resource"
      )

      expect(buttonsInTopRow).toHaveLength(1)
      expect(toggleDisableButton).toBeInTheDocument()
      expect(
        within(topRow).queryByLabelText("Trigger Disable Resource")
      ).toBeNull()
    })
  })

  describe("term filter input", () => {
    it("renders with no initial value if there is no existing term filter", () => {
      customRender(<FullBar />)

      expect(
        screen.getByRole("textbox", { name: /filter resource logs/i })
      ).toHaveValue("")
    })

    it("renders with an initial value if there is an existing term filter", () => {
      customRender(<FullBar />, { initialEntries: ["/?term=bleep+bloop"] })

      expect(
        screen.getByRole("textbox", { name: /filter resource logs/i })
      ).toHaveValue("bleep bloop")
    })

    it("changes the global term filter state when its value changes", () => {
      jest.useFakeTimers()

      customRender(<FullBar />)

      userEvent.type(screen.getByRole("textbox"), "docker")

      act(() => {
        jest.advanceTimersByTime(FILTER_INPUT_DEBOUNCE)
      })

      expect(getSearch()).toEqual("?term=docker")
    })

    it("uses debouncing to update the global term filter state", () => {
      jest.useFakeTimers()

      customRender(<FullBar />)

      userEvent.type(screen.getByRole("textbox"), "doc")

      act(() => {
        jest.advanceTimersByTime(FILTER_INPUT_DEBOUNCE / 2)
      })

      // The debouncing time hasn't passed yet, so we don't expect to see any changes
      expect(getSearch()).toEqual("")

      userEvent.type(screen.getByRole("textbox"), "ker")

      // The debouncing time hasn't passed yet, so we don't expect to see any changes
      expect(getSearch()).toEqual("")

      act(() => {
        jest.advanceTimersByTime(FILTER_INPUT_DEBOUNCE)
      })

      // Since the debouncing time has passed, we expect to see the final
      // change reflected
      expect(getSearch()).toEqual("?term=docker")
    })

    it("retains any current level and source filters when its value changes", () => {
      jest.useFakeTimers()

      customRender(<FullBar />, {
        initialEntries: ["/?level=warn&source=build"],
      })

      userEvent.type(screen.getByRole("textbox"), "help")

      act(() => {
        jest.advanceTimersByTime(FILTER_INPUT_DEBOUNCE)
      })

      expect(getSearch()).toEqual("?level=warn&source=build&term=help")
    })
  })

  describe("createLogSearch", () => {
    let currentSearch: URLSearchParams
    beforeEach(() => (currentSearch = new URLSearchParams()))

    it("sets the params that are passed in", () => {
      expect(
        createLogSearch(currentSearch.toString(), {
          level: FilterLevel.all,
          term: "find me",
          source: FilterSource.build,
        }).toString()
      ).toBe("source=build&term=find+me")

      expect(
        createLogSearch(currentSearch.toString(), {
          level: FilterLevel.warn,
        }).toString()
      ).toBe("level=warn")

      expect(
        createLogSearch(currentSearch.toString(), {
          term: "",
          source: FilterSource.runtime,
        }).toString()
      ).toBe("source=runtime")
    })

    it("overrides params if a new value is defined", () => {
      currentSearch.set("level", FilterLevel.warn)
      expect(
        createLogSearch(currentSearch.toString(), {
          level: FilterLevel.error,
        }).toString()
      ).toBe("level=error")
      currentSearch.delete("level")

      currentSearch.set("level", "a meaningless value")
      currentSearch.set("term", "")
      expect(
        createLogSearch(currentSearch.toString(), {
          level: FilterLevel.all,
          term: "service",
        }).toString()
      ).toBe("term=service")
    })

    it("preserves existing params if no new value is defined", () => {
      currentSearch.set("source", FilterSource.build)
      expect(
        createLogSearch(currentSearch.toString(), {
          term: "test",
        }).toString()
      ).toBe("source=build&term=test")
    })
  })
})