github.com/tilt-dev/tilt@v0.36.0/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<any>
   186      let container: HTMLDivElement
   187      let component: OverviewLogComponent
   188  
   189      beforeEach(() => {
   190        fakeRaf = newFakeRaf()
   191  
   192        class ManyLinesWrapper extends Component {
   193          render() {
   194            return (
   195              <MemoryRouter initialEntries={["/"]}>
   196                <RafProvider value={fakeRaf}>
   197                  <ManyLines count={initLineCount} />
   198                </RafProvider>
   199              </MemoryRouter>
   200            )
   201          }
   202        }
   203  
   204        const testHelpers = renderTestComponent(<ManyLinesWrapper />)
   205        rootTree = testHelpers.rootTree
   206        container = testHelpers.container
   207        component = findRenderedComponentWithType(rootTree, OverviewLogComponent)
   208      })
   209  
   210      it("engages autoscrolls on scroll down", () => {
   211        component.autoscroll = false
   212        component.scrollTop = 0
   213        component.rootRef.current.scrollTop = 1000
   214        component.onScroll()
   215        expect(component.scrollTop).toEqual(1000)
   216  
   217        // The scroll has been scheduled, but not engaged yet.
   218        expect(component.autoscrollRafId).toBeGreaterThan(0)
   219        expect(component.autoscroll).toEqual(false)
   220  
   221        fakeRaf.invoke(component.autoscrollRafId as number)
   222        expect(component.autoscroll).toEqual(true)
   223      })
   224  
   225      it("renders bottom logs first", () => {
   226        // Make sure no logs have been rendered yet.
   227        let getLogElements = () => container.querySelectorAll(".LogLine")
   228  
   229        expect(component.renderBufferRafId).toBeGreaterThan(0)
   230        expect(component.backwardBuffer.length).toEqual(initLineCount)
   231        expect(getLogElements().length).toEqual(0)
   232  
   233        // Invoke the RAF callback, and make sure that only a window's
   234        // worth of logs have been rendered.
   235        fakeRaf.invoke(component.renderBufferRafId as number)
   236        expect(component.backwardBuffer.length).toEqual(
   237          initLineCount - renderWindow
   238        )
   239        expect(getLogElements().length).toEqual(renderWindow)
   240        expect(getLogElements()[0].innerHTML).toEqual(
   241          expect.stringContaining(">line 250\n<")
   242        )
   243  
   244        // Invoke the RAF callback again, and make sure the remaining logs
   245        // were rendered.
   246        fakeRaf.invoke(component.renderBufferRafId as number)
   247        expect(component.backwardBuffer.length).toEqual(0)
   248        expect(getLogElements().length).toEqual(initLineCount)
   249        expect(getLogElements()[0].innerHTML).toEqual(
   250          expect.stringContaining(">line 0\n<")
   251        )
   252  
   253        // rendering is complete.
   254        expect(component.renderBufferRafId).toEqual(0)
   255      })
   256  
   257      it("renders new logs first", () => {
   258        expect(component.renderBufferRafId).toBeGreaterThan(0)
   259        expect(component.backwardBuffer.length).toEqual(initLineCount)
   260        expect(getLogElements(container).length).toEqual(0)
   261  
   262        // append new lines on top of the lines we already have.
   263        let newLineCount = 1.5 * renderWindow
   264        let lines = []
   265        for (let i = 0; i < newLineCount; i++) {
   266          lines.push(`incremental line ${i}\n`)
   267        }
   268        appendLines(component.props.logStore, "fe", ...lines)
   269        component.onLogUpdate({ action: LogUpdateAction.append })
   270        expect(component.forwardBuffer.length).toEqual(newLineCount)
   271        expect(component.backwardBuffer.length).toEqual(initLineCount)
   272  
   273        // Invoke the RAF callback, and make sure that new logs were rendered
   274        // and old logs were rendered.
   275        fakeRaf.invoke(component.renderBufferRafId as number)
   276        expect(component.forwardBuffer.length).toEqual(
   277          newLineCount - renderWindow
   278        )
   279        expect(component.backwardBuffer.length).toEqual(
   280          initLineCount - renderWindow
   281        )
   282  
   283        const logElements = getLogElements(container)
   284        expect(logElements.length).toEqual(initLineCount)
   285        expect(logElements[0].innerHTML).toEqual(
   286          expect.stringContaining(">line 250\n<")
   287        )
   288        expect(logElements[logElements.length - 1].innerHTML).toEqual(
   289          expect.stringContaining(">incremental line 249\n<")
   290        )
   291  
   292        // Invoke the RAF callback again, and make sure that new logs were rendered further up
   293        // and old logs were rendered further down.
   294        fakeRaf.invoke(component.renderBufferRafId as number)
   295        const logElementsAfterInvoke = getLogElements(container)
   296        expect(logElementsAfterInvoke[0].innerHTML).toEqual(
   297          expect.stringContaining(">line 0\n<")
   298        )
   299        expect(
   300          logElementsAfterInvoke[logElementsAfterInvoke.length - 1].innerHTML
   301        ).toEqual(expect.stringContaining(">incremental line 374\n<"))
   302      })
   303    })
   304  })