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