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", "➜  Local:   http://localhost:5173/\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  })