github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/web/src/HUD.test.tsx (about)

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