github.com/tilt-dev/tilt@v0.36.0/web/src/HUD.test.tsx (about)

     1  import React, { Component } from "react"
     2  import { findRenderedComponentWithType } from "react-dom/test-utils"
     3  import ReactModal from "react-modal"
     4  import { useNavigate, useLocation } from "react-router-dom"
     5  import { MemoryRouter } from "react-router"
     6  import HUD, { mergeAppUpdate } from "./HUD"
     7  import LogStore from "./LogStore"
     8  import { SocketBarRoot } from "./SocketBar"
     9  import { renderTestComponent } from "./test-helpers"
    10  import {
    11    logList,
    12    nButtonView,
    13    nResourceView,
    14    oneResourceView,
    15    twoResourceView,
    16  } from "./testdata"
    17  import { SocketState } from "./types"
    18  
    19  // Note: `body` is used as the app element _only_ in a test env
    20  // since the app root element isn't available; in prod, it should
    21  // be set as the app root so that accessibility features are set correctly
    22  ReactModal.setAppElement(document.body)
    23  
    24  const interfaceVersion = { isNewDefault: () => false, toggleDefault: () => {} }
    25  
    26  let InjectHUD = () => {
    27    let navigate = useNavigate()
    28    let location = useLocation()
    29    return (
    30      <HUD
    31        navigate={navigate}
    32        location={location}
    33        interfaceVersion={interfaceVersion}
    34      />
    35    )
    36  }
    37  
    38  class RouterHUD extends Component {
    39    render() {
    40      return (
    41        <MemoryRouter initialEntries={["/"]}>
    42          <InjectHUD />
    43        </MemoryRouter>
    44      )
    45    }
    46  }
    47  
    48  beforeEach(() => {
    49    Date.now = jest.fn(() => 1482363367071)
    50  })
    51  
    52  it("renders reconnecting bar", async () => {
    53    const { rootTree, container } = renderTestComponent(<RouterHUD />)
    54    expect(container.textContent).toEqual(expect.stringContaining("Loading"))
    55  
    56    const hud = findRenderedComponentWithType(rootTree, HUD)
    57  
    58    hud.setState({
    59      view: oneResourceView(),
    60      socketState: SocketState.Reconnecting,
    61    })
    62  
    63    let socketBar = Array.from(container.querySelectorAll(SocketBarRoot))
    64    expect(socketBar).toHaveLength(1)
    65    expect(socketBar[0].textContent).toEqual(
    66      expect.stringContaining("reconnecting")
    67    )
    68  })
    69  
    70  it("loads logs incrementally", async () => {
    71    const { rootTree } = renderTestComponent(<RouterHUD />)
    72    const hud = findRenderedComponentWithType(rootTree, HUD)
    73  
    74    let now = new Date().toString()
    75    let resourceView = oneResourceView()
    76    resourceView.logList = {
    77      spans: {
    78        "": {},
    79      },
    80      segments: [
    81        { text: "line1\n", time: now },
    82        { text: "line2\n", time: now },
    83      ],
    84      fromCheckpoint: 0,
    85      toCheckpoint: 2,
    86    }
    87    hud.onAppChange({ view: resourceView })
    88  
    89    let resourceView2 = oneResourceView()
    90    resourceView2.logList = {
    91      spans: {
    92        "": {},
    93      },
    94      segments: [
    95        { text: "line3\n", time: now },
    96        { text: "line4\n", time: now },
    97      ],
    98      fromCheckpoint: 2,
    99      toCheckpoint: 4,
   100    }
   101    hud.onAppChange({ view: resourceView2 })
   102  
   103    let snapshot = hud.snapshotFromState(hud.state)
   104    expect(snapshot.view?.logList).toEqual({
   105      spans: {
   106        _: { manifestName: "" },
   107      },
   108      segments: [
   109        { text: "line1\n", time: now, spanId: "_" },
   110        { text: "line2\n", time: now, spanId: "_" },
   111        { text: "line3\n", time: now, spanId: "_" },
   112        { text: "line4\n", time: now, spanId: "_" },
   113      ],
   114    })
   115  })
   116  
   117  it("renders logs to snapshot", async () => {
   118    const { rootTree } = renderTestComponent(<RouterHUD />)
   119    const hud = findRenderedComponentWithType(rootTree, HUD)
   120  
   121    let now = new Date().toString()
   122    let resourceView = oneResourceView()
   123    resourceView.logList = {
   124      spans: {
   125        "": {},
   126      },
   127      segments: [
   128        { text: "line1\n", time: now, level: "WARN" },
   129        { text: "line2\n", time: now, fields: { buildEvent: "1" } },
   130      ],
   131      fromCheckpoint: 0,
   132      toCheckpoint: 2,
   133    }
   134    hud.onAppChange({ view: resourceView })
   135  
   136    let snapshot = hud.snapshotFromState(hud.state)
   137    expect(snapshot.view?.logList).toEqual({
   138      spans: {
   139        _: { manifestName: "" },
   140      },
   141      segments: [
   142        { text: "line1\n", time: now, spanId: "_", level: "WARN" },
   143        { text: "line2\n", time: now, spanId: "_", fields: { buildEvent: "1" } },
   144      ],
   145    })
   146  })
   147  
   148  describe("mergeAppUpdates", () => {
   149    // It's important to maintain reference equality when nothing changes.
   150    it("handles no view update", () => {
   151      let resourceView = oneResourceView()
   152      let prevState = { view: resourceView }
   153      let result = mergeAppUpdate(prevState as any, {}) as any
   154      expect(result).toBe(null)
   155    })
   156  
   157    it("handles empty view update", () => {
   158      let resourceView = oneResourceView()
   159      let prevState = { view: resourceView }
   160      let result = mergeAppUpdate(prevState as any, { view: {} })
   161      expect(result).toBe(null)
   162    })
   163  
   164    it("handles replace view update", () => {
   165      let prevState = { view: oneResourceView() }
   166      let update = { view: oneResourceView() }
   167      let result = mergeAppUpdate(prevState as any, update)
   168      expect(result!.view).not.toBe(update.view)
   169      expect(result!.view).not.toBe(prevState.view)
   170      expect(result!.view.uiSession).toBe(update.view.uiSession)
   171    })
   172  
   173    it("handles add resource", () => {
   174      let prevState = { view: oneResourceView() }
   175      let update = { view: { uiResources: [twoResourceView().uiResources[1]] } }
   176      let result = mergeAppUpdate(prevState as any, update)
   177      expect(result!.view).not.toBe(prevState.view)
   178      expect(result!.view.uiSession).toBe(prevState.view.uiSession)
   179      expect(result!.view.uiResources!.length).toEqual(2)
   180      expect(result!.view.uiResources![0].metadata!.name).toEqual("vigoda")
   181      expect(result!.view.uiResources![1].metadata!.name).toEqual("snack")
   182    })
   183  
   184    it("handles add resource out of order", () => {
   185      let prevState = { view: nResourceView(10) }
   186      let addedResources = prevState.view.uiResources.splice(3, 1)
   187  
   188      let update = { view: { uiResources: addedResources } }
   189      let result = mergeAppUpdate(prevState as any, update)
   190      expect(result!.view).not.toBe(prevState.view)
   191      expect(result!.view.uiSession).toBe(prevState.view.uiSession)
   192      expect(result!.view.uiResources).toEqual(nResourceView(10).uiResources)
   193    })
   194  
   195    it("handles add button out of order", () => {
   196      let prevState = { view: nButtonView(9) }
   197      let addedButtons = prevState.view.uiButtons.splice(3, 1)
   198  
   199      let update = { view: { uiButtons: addedButtons } }
   200      let result = mergeAppUpdate(prevState as any, update)
   201      expect(result!.view).not.toBe(prevState.view)
   202      expect(result!.view.uiSession).toBe(prevState.view.uiSession)
   203      expect(result!.view.uiButtons).toEqual(nButtonView(9).uiButtons)
   204    })
   205  
   206    it("handles delete resource", () => {
   207      let prevState = { view: twoResourceView() }
   208      let update = {
   209        view: {
   210          uiResources: [
   211            {
   212              metadata: {
   213                name: "vigoda",
   214                deletionTimestamp: new Date().toString(),
   215              },
   216            },
   217          ],
   218        },
   219      }
   220      let result = mergeAppUpdate(prevState as any, update)
   221      expect(result!.view).not.toBe(prevState.view)
   222      expect(result!.view.uiResources!.length).toEqual(1)
   223      expect(result!.view.uiResources![0].metadata!.name).toEqual("snack")
   224    })
   225  
   226    it("handles replace resource", () => {
   227      let prevState = { view: twoResourceView() }
   228      let update = { view: { uiResources: [{ metadata: { name: "vigoda" } }] } }
   229      let result = mergeAppUpdate(prevState as any, update)
   230      expect(result!.view).not.toBe(prevState.view)
   231      expect(result!.view.uiResources!.length).toEqual(2)
   232      expect(result!.view.uiResources![0]).toBe(update.view.uiResources[0])
   233      expect(result!.view.uiResources![1]).toBe(prevState.view.uiResources[1])
   234    })
   235  
   236    it("handles add button", () => {
   237      let prevState = { view: nButtonView(1) }
   238      let update = { view: { uiButtons: [nButtonView(2).uiButtons[1]] } }
   239      let result = mergeAppUpdate(prevState as any, update)
   240      expect(result!.view).not.toBe(prevState.view)
   241      expect(result!.view.uiSession).toBe(prevState.view.uiSession)
   242      expect(result!.view.uiResources).toBe(prevState.view.uiResources)
   243      expect(result!.view.uiButtons!.length).toEqual(2)
   244      expect(result!.view.uiButtons![0].metadata!.name).toEqual("button1")
   245      expect(result!.view.uiButtons![1].metadata!.name).toEqual("button2")
   246    })
   247  
   248    it("handles delete button", () => {
   249      let prevState = { view: nButtonView(2) }
   250      let update = {
   251        view: {
   252          uiButtons: [
   253            {
   254              metadata: {
   255                name: "button1",
   256                deletionTimestamp: new Date().toString(),
   257              },
   258            },
   259          ],
   260        },
   261      }
   262      let result = mergeAppUpdate(prevState as any, update)
   263      expect(result!.view).not.toBe(prevState.view)
   264      expect(result!.view.uiResources).toBe(prevState.view.uiResources)
   265      expect(result!.view.uiButtons!.length).toEqual(1)
   266      expect(result!.view.uiButtons![0].metadata!.name).toEqual("button2")
   267    })
   268  
   269    it("handles replace button", () => {
   270      let prevState = { view: nButtonView(2) }
   271      let update = { view: { uiButtons: [{ metadata: { name: "button1" } }] } }
   272      let result = mergeAppUpdate(prevState as any, update)
   273      expect(result!.view).not.toBe(prevState.view)
   274      expect(result!.view.uiResources).toBe(prevState.view.uiResources)
   275      expect(result!.view.uiButtons!.length).toEqual(2)
   276      expect(result!.view.uiButtons![0]).toBe(update.view.uiButtons[0])
   277      expect(result!.view.uiButtons![1]).toBe(prevState.view.uiButtons[1])
   278    })
   279  
   280    it("handles socket state", () => {
   281      let prevState = { view: twoResourceView(), socketState: SocketState.Active }
   282      let update = { socketState: SocketState.Reconnecting }
   283      let result = mergeAppUpdate(prevState as any, update) as any
   284      expect(result!.view).toBe(prevState.view)
   285      expect(result!.socketState).toBe(SocketState.Reconnecting)
   286    })
   287  
   288    it("handles complete view", () => {
   289      let prevLogStore = new LogStore()
   290      let prevState = { view: twoResourceView(), logStore: prevLogStore }
   291  
   292      let update = {
   293        view: {
   294          uiResources: [{ metadata: { name: "b" } }, { metadata: { name: "a" } }],
   295          uiButtons: [{ metadata: { name: "z" } }, { metadata: { name: "y" } }],
   296          logList: logList(["line1", "line2"]),
   297          isComplete: true,
   298        },
   299      }
   300      let result = mergeAppUpdate<"view" | "logStore">(prevState as any, update)
   301      expect(result!.view).toBe(update.view)
   302      expect(result!.logStore).not.toBe(prevState.logStore)
   303      expect(result!.logStore?.allLog().map((ll) => ll.text)).toEqual([
   304        "line1",
   305        "line2",
   306      ])
   307      const expectedResourceOrder = ["a", "b"]
   308      expect(result!.view.uiResources?.map((r) => r.metadata!.name)).toEqual(
   309        expectedResourceOrder
   310      )
   311      const expectedButtonOrder = ["y", "z"]
   312      expect(result!.view.uiButtons?.map((r) => r.metadata!.name)).toEqual(
   313        expectedButtonOrder
   314      )
   315    })
   316  
   317    it("handles log only update", () => {
   318      let prevLogStore = new LogStore()
   319      let prevState = { view: twoResourceView(), logStore: prevLogStore }
   320  
   321      let update = {
   322        view: {
   323          logList: logList(["line1", "line2"]),
   324        },
   325      }
   326      let result = mergeAppUpdate<"view" | "logStore">(prevState as any, update)
   327      expect(result).toBe(null)
   328    })
   329  })