github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/web/src/OverviewLogPane.test.tsx (about) 1 import { render, RenderOptions, screen } from "@testing-library/react" 2 import { Component } from "react" 3 import { findRenderedComponentWithType } from "react-dom/test-utils" 4 import { MemoryRouter } from "react-router" 5 import { 6 createFilterTermState, 7 EMPTY_FILTER_TERM, 8 FilterLevel, 9 FilterSource, 10 } from "./logfilters" 11 import LogStore, { LogUpdateAction, LogStoreProvider } from "./LogStore" 12 import OverviewLogPane, { 13 OverviewLogComponent, 14 PROLOGUE_LENGTH, 15 renderWindow, 16 } from "./OverviewLogPane" 17 import { 18 BuildLogAndRunLog, 19 ManyLines, 20 StyledLines, 21 ThreeLines, 22 ThreeLinesAllLog, 23 StarredResourcesLog, 24 } from "./OverviewLogPane.stories" 25 import { newFakeRaf, RafProvider, SyncRafProvider, TestRafContext } from "./raf" 26 import { renderTestComponent } from "./test-helpers" 27 import { appendLines } from "./testlogs" 28 29 function customRender(component: JSX.Element, options?: RenderOptions) { 30 return render(component, { 31 wrapper: ({ children }) => ( 32 <MemoryRouter initialEntries={["/"]}> 33 <SyncRafProvider>{children}</SyncRafProvider> 34 </MemoryRouter> 35 ), 36 ...options, 37 }) 38 } 39 40 describe("OverviewLogPane", () => { 41 it("renders all log lines associated with a specific resource", () => { 42 const { container } = customRender(<ThreeLines />) 43 expect(container.querySelectorAll(".LogLine")).toHaveLength(3) 44 }) 45 46 it("renders all log lines in the all log view", () => { 47 const { container } = customRender(<ThreeLinesAllLog />) 48 expect(container.querySelectorAll(".LogLine")).toHaveLength(3) 49 }) 50 51 it("renders log lines of starred resources", () => { 52 const { container } = customRender(<StarredResourcesLog />) 53 expect(container.querySelectorAll(".LogLine")).toHaveLength(9) 54 }) 55 56 it("escapes html and linkifies", () => { 57 customRender(<StyledLines />) 58 expect(screen.getAllByRole("link")).toHaveLength(3) 59 expect(screen.queryByRole("button")).toBeNull() 60 }) 61 62 it("properly escapes ansi chars", () => { 63 let defaultFilter = { 64 source: FilterSource.all, 65 level: FilterLevel.all, 66 term: EMPTY_FILTER_TERM, 67 } 68 let logStore = new LogStore() 69 appendLines(logStore, "fe", "[32m➜[39m [1mLocal[22m: [36mhttp://localhost:[1m5173[22m/[39m\n") 70 const { container } = customRender( 71 <LogStoreProvider value={logStore}> 72 <OverviewLogPane manifestName="fe" filterSet={defaultFilter} /> 73 </LogStoreProvider> 74 ) 75 expect(container.querySelectorAll(".LogLine")).toHaveLength(1) 76 expect(container.querySelector(".LogLine")).toHaveTextContent( 77 "➜ Local: http://localhost:5173/" 78 ) 79 }) 80 81 it("displays all logs when there are no filters", () => { 82 const { container } = customRender(<BuildLogAndRunLog />) 83 expect(container.querySelectorAll(".LogLine")).toHaveLength(40) 84 }) 85 86 describe("filters by source", () => { 87 it("displays only runtime logs when runtime source is specified", () => { 88 const { container } = customRender( 89 <BuildLogAndRunLog 90 level="" 91 source={FilterSource.runtime} 92 term={EMPTY_FILTER_TERM} 93 /> 94 ) 95 expect(container.querySelectorAll(".LogLine")).toHaveLength(20) 96 expect(screen.getAllByText(/Vigoda pod line/)).toHaveLength(18) 97 expect(screen.queryByText(/Vigoda build line/)).toBeNull() 98 }) 99 100 it("displays only build logs when build source is specified", () => { 101 const { container } = customRender( 102 <BuildLogAndRunLog 103 level="" 104 source={FilterSource.build} 105 term={EMPTY_FILTER_TERM} 106 /> 107 ) 108 expect(container.querySelectorAll(".LogLine")).toHaveLength(20) 109 expect(screen.getAllByText(/Vigoda build line/)).toHaveLength(18) 110 expect(screen.queryByText(/Vigoda pod line/)).toBeNull() 111 }) 112 }) 113 114 describe("filters by level", () => { 115 it("displays only warning logs when warning log level is specified", () => { 116 const { container } = customRender( 117 <BuildLogAndRunLog 118 level={FilterLevel.warn} 119 source="" 120 term={EMPTY_FILTER_TERM} 121 /> 122 ) 123 expect(container.querySelectorAll(".LogLine")).toHaveLength( 124 2 * (1 + PROLOGUE_LENGTH) 125 ) 126 const alerts = container.querySelectorAll(".is-endOfAlert") 127 const lastAlert = alerts[alerts.length - 1] 128 expect(lastAlert).toHaveTextContent("Vigoda pod warning line") 129 expect(screen.queryByText(/Vigoda pod error line/)).toBeNull() 130 }) 131 132 it("displays only error logs when error log level is specified", () => { 133 const { container } = customRender( 134 <BuildLogAndRunLog 135 level={FilterLevel.error} 136 source="" 137 term={EMPTY_FILTER_TERM} 138 /> 139 ) 140 141 expect(container.querySelectorAll(".LogLine")).toHaveLength( 142 2 * (1 + PROLOGUE_LENGTH) 143 ) 144 const alerts = container.querySelectorAll(".is-endOfAlert") 145 const lastAlert = alerts[alerts.length - 1] 146 expect(lastAlert).toHaveTextContent("Vigoda pod error line") 147 }) 148 }) 149 150 describe("filters by term", () => { 151 it("displays log lines that match the specified filter term", () => { 152 const termWithResults = createFilterTermState("line 5") 153 const { container } = customRender( 154 <BuildLogAndRunLog source="" level="" term={termWithResults} /> 155 ) 156 157 expect(container.querySelectorAll(".LogLine")).toHaveLength(2) 158 expect(screen.getAllByText(/line 5/)).toHaveLength(2) 159 expect(screen.queryByText(/line 15/)).toBeNull() 160 }) 161 162 it("displays zero log lines when no logs match the specified filter term", () => { 163 const termWithResults = createFilterTermState("spaghetti") 164 const { container } = customRender( 165 <BuildLogAndRunLog source="" level="" term={termWithResults} /> 166 ) 167 168 expect(container.querySelectorAll(".LogLine")).toHaveLength(0) 169 }) 170 }) 171 172 /** 173 * The following tests rely on testing React component state directly, 174 * which is not possible to do with React Testing Library. 175 */ 176 177 describe("log rendering", () => { 178 function getLogElements(container: HTMLElement) { 179 return container.querySelectorAll(".LogLine") 180 } 181 182 const initLineCount = 2 * renderWindow 183 184 let fakeRaf: TestRafContext 185 let rootTree: Component<OverviewLogComponent> 186 let container: HTMLDivElement 187 let component: OverviewLogComponent 188 189 beforeEach(() => { 190 fakeRaf = newFakeRaf() 191 const componentWrapper = ( 192 <MemoryRouter initialEntries={["/"]}> 193 <RafProvider value={fakeRaf}> 194 <ManyLines count={initLineCount} /> 195 </RafProvider> 196 </MemoryRouter> 197 ) 198 199 const testHelpers = 200 renderTestComponent<OverviewLogComponent>(componentWrapper) 201 rootTree = testHelpers.rootTree 202 container = testHelpers.container 203 component = findRenderedComponentWithType(rootTree, OverviewLogComponent) 204 }) 205 206 it("engages autoscrolls on scroll down", () => { 207 component.autoscroll = false 208 component.scrollTop = 0 209 component.rootRef.current.scrollTop = 1000 210 component.onScroll() 211 expect(component.scrollTop).toEqual(1000) 212 213 // The scroll has been scheduled, but not engaged yet. 214 expect(component.autoscrollRafId).toBeGreaterThan(0) 215 expect(component.autoscroll).toEqual(false) 216 217 fakeRaf.invoke(component.autoscrollRafId as number) 218 expect(component.autoscroll).toEqual(true) 219 }) 220 221 it("renders bottom logs first", () => { 222 // Make sure no logs have been rendered yet. 223 let getLogElements = () => container.querySelectorAll(".LogLine") 224 225 expect(component.renderBufferRafId).toBeGreaterThan(0) 226 expect(component.backwardBuffer.length).toEqual(initLineCount) 227 expect(getLogElements().length).toEqual(0) 228 229 // Invoke the RAF callback, and make sure that only a window's 230 // worth of logs have been rendered. 231 fakeRaf.invoke(component.renderBufferRafId as number) 232 expect(component.backwardBuffer.length).toEqual( 233 initLineCount - renderWindow 234 ) 235 expect(getLogElements().length).toEqual(renderWindow) 236 expect(getLogElements()[0].innerHTML).toEqual( 237 expect.stringContaining(">line 250\n<") 238 ) 239 240 // Invoke the RAF callback again, and make sure the remaining logs 241 // were rendered. 242 fakeRaf.invoke(component.renderBufferRafId as number) 243 expect(component.backwardBuffer.length).toEqual(0) 244 expect(getLogElements().length).toEqual(initLineCount) 245 expect(getLogElements()[0].innerHTML).toEqual( 246 expect.stringContaining(">line 0\n<") 247 ) 248 249 // rendering is complete. 250 expect(component.renderBufferRafId).toEqual(0) 251 }) 252 253 it("renders new logs first", () => { 254 expect(component.renderBufferRafId).toBeGreaterThan(0) 255 expect(component.backwardBuffer.length).toEqual(initLineCount) 256 expect(getLogElements(container).length).toEqual(0) 257 258 // append new lines on top of the lines we already have. 259 let newLineCount = 1.5 * renderWindow 260 let lines = [] 261 for (let i = 0; i < newLineCount; i++) { 262 lines.push(`incremental line ${i}\n`) 263 } 264 appendLines(component.props.logStore, "fe", ...lines) 265 component.onLogUpdate({ action: LogUpdateAction.append }) 266 expect(component.forwardBuffer.length).toEqual(newLineCount) 267 expect(component.backwardBuffer.length).toEqual(initLineCount) 268 269 // Invoke the RAF callback, and make sure that new logs were rendered 270 // and old logs were rendered. 271 fakeRaf.invoke(component.renderBufferRafId as number) 272 expect(component.forwardBuffer.length).toEqual( 273 newLineCount - renderWindow 274 ) 275 expect(component.backwardBuffer.length).toEqual( 276 initLineCount - renderWindow 277 ) 278 279 const logElements = getLogElements(container) 280 expect(logElements.length).toEqual(initLineCount) 281 expect(logElements[0].innerHTML).toEqual( 282 expect.stringContaining(">line 250\n<") 283 ) 284 expect(logElements[logElements.length - 1].innerHTML).toEqual( 285 expect.stringContaining(">incremental line 249\n<") 286 ) 287 288 // Invoke the RAF callback again, and make sure that new logs were rendered further up 289 // and old logs were rendered further down. 290 fakeRaf.invoke(component.renderBufferRafId as number) 291 const logElementsAfterInvoke = getLogElements(container) 292 expect(logElementsAfterInvoke[0].innerHTML).toEqual( 293 expect.stringContaining(">line 0\n<") 294 ) 295 expect( 296 logElementsAfterInvoke[logElementsAfterInvoke.length - 1].innerHTML 297 ).toEqual(expect.stringContaining(">incremental line 374\n<")) 298 }) 299 }) 300 })