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 })