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, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import React, { ChangeEvent, useState } from "react"
import {
  ResourceSelectionProvider,
  useResourceSelection,
} from "./ResourceSelectionContext"

const SELECTION_STATE = "selected-state"
const IS_SELECTED = "selected-text"
const TO_SELECT_INPUT = "select-input"
const SELECT_BUTTON = "select-button"
const DESELECT_BUTTON = "deselect-button"
const CLEAR_BUTTON = "clear-button"

// This is a very basic test component that prints out the state
// from the ResourceSelection context and provides buttons to trigger
// methods returned by the context, so they can be tested
const TestConsumer = () => {
  const { selected, isSelected, select, deselect, clearSelections } =
    useResourceSelection()

  const [value, setValue] = useState("")
  const onChange = (e: ChangeEvent<HTMLInputElement>) =>
    setValue(e.target.value)

  // To support selecting multiple items at once, accept a comma separated list
  const parsedValues = (value: string) => value.split(",")
  return (
    <>
      {/* Print the `selected` state */}
      <p aria-label={SELECTION_STATE}>{JSON.stringify(Array.from(selected))}</p>
      {/* Use an input field to change the resource that can be selected/deselected */}
      <input aria-label={TO_SELECT_INPUT} type="text" onChange={onChange} />
      {/* Print the state for whatever resource is currently in the `input` */}
      <p aria-label={IS_SELECTED}>{isSelected(value).toString()}</p>
      {/* Select the resource that's currently in the `input` */}
      <button
        aria-label={SELECT_BUTTON}
        onClick={() => select(...parsedValues(value))}
      >
        Select
      </button>
      {/* Deselect the resource that's currently in the `input` */}
      <button
        aria-label={DESELECT_BUTTON}
        onClick={() => deselect(...parsedValues(value))}
      >
        Deselect
      </button>
      {/* Clear all selections */}
      <button aria-label={CLEAR_BUTTON} onClick={() => clearSelections()}>
        Clear selections
      </button>
    </>
  )
}

describe("ResourceSelectionContext", () => {
  // Helpers
  const INITIAL_SELECTIONS = ["vigoda", "magic_beans", "servantes"]
  const selectedState = () =>
    screen.queryByLabelText(SELECTION_STATE)?.textContent
  const isSelected = () => screen.queryByLabelText(IS_SELECTED)?.textContent
  const setCurrentResource = (value: string) => {
    const inputField = screen.queryByLabelText(TO_SELECT_INPUT)
    userEvent.type(inputField as HTMLInputElement, value)
  }
  const selectResource = (value: string) => {
    setCurrentResource(value)
    const selectButton = screen.queryByLabelText(SELECT_BUTTON)
    userEvent.click(selectButton as HTMLButtonElement)
  }
  const deselectResource = (value: string) => {
    setCurrentResource(value)
    const deselectButton = screen.queryByLabelText(DESELECT_BUTTON)
    userEvent.click(deselectButton as HTMLButtonElement)
  }
  const clearResources = () => {
    const clearButton = screen.queryByLabelText(CLEAR_BUTTON)
    userEvent.click(clearButton as HTMLButtonElement)
  }

  beforeEach(() => {
    render(
      <ResourceSelectionProvider initialValuesForTesting={INITIAL_SELECTIONS}>
        <TestConsumer />
      </ResourceSelectionProvider>
    )
  })

  describe("`selected` property", () => {
    it("reports an accurate list of selected resources", () => {
      expect(selectedState()).toBe(JSON.stringify(INITIAL_SELECTIONS))

      clearResources()
      expect(selectedState()).toBe(JSON.stringify([]))
    })
  })

  describe("isSelected", () => {
    it("returns `true` when a resource is selected", () => {
      setCurrentResource(INITIAL_SELECTIONS[1])
      expect(isSelected()).toBe("true")
    })

    it("returns `false` when a resource is NOT selected", () => {
      setCurrentResource("sprout")
      expect(isSelected()).toBe("false")
    })
  })

  describe("select", () => {
    it("adds a resource to the list of selections", () => {
      selectResource("cool_beans")
      expect(isSelected()).toBe("true")
      expect(selectedState()).toBe(
        JSON.stringify([...INITIAL_SELECTIONS, "cool_beans"])
      )
    })

    it("adds multiple resources to the list of selections", () => {
      selectResource("cool_beans,super_beans,fancy_beans")
      expect(selectedState()).toBe(
        JSON.stringify([
          ...INITIAL_SELECTIONS,
          "cool_beans",
          "super_beans",
          "fancy_beans",
        ])
      )
    })

    it("does NOT select the same resource twice", () => {
      selectResource(INITIAL_SELECTIONS[0])
      expect(isSelected()).toBe("true")
      expect(selectedState()).toBe(JSON.stringify(INITIAL_SELECTIONS))
    })
  })

  describe("deselect", () => {
    it("removes a resource from the list of selections", () => {
      deselectResource(INITIAL_SELECTIONS[0])
      expect(isSelected()).toBe("false")
      expect(selectedState()).toBe(JSON.stringify(INITIAL_SELECTIONS.slice(1)))
    })

    it("removes multiple resources from the list of selections", () => {
      deselectResource(`${INITIAL_SELECTIONS[0]},${INITIAL_SELECTIONS[1]}`)
      expect(selectedState()).toBe(JSON.stringify([INITIAL_SELECTIONS[2]]))
    })
  })

  describe("clearSelections", () => {
    it("removes all resource selections", () => {
      clearResources()
      expect(selectedState()).toBe(JSON.stringify([]))
    })
  })

  it("memoizes renders", () => {
    let renderCount = 0
    let selection: any
    let FakeEl = React.memo(() => {
      selection = useResourceSelection()
      renderCount++
      return <div></div>
    })

    let tree = () => {
      return (
        <ResourceSelectionProvider initialValuesForTesting={INITIAL_SELECTIONS}>
          <FakeEl />
        </ResourceSelectionProvider>
      )
    }

    let { rerender } = render(tree())

    expect(renderCount).toEqual(1)
    rerender(tree())

    // Make sure we don't re-render on a no-op update.
    expect(renderCount).toEqual(1)

    act(() => selection.clearSelections())
    expect(renderCount).toEqual(2)
  })
})