ElectronicBabylonianLiterature/ebl-frontend

View on GitHub
src/http/withData.test.tsx

Summary

Maintainability
A
1 hr
Test Coverage
import React from 'react'
import { render, RenderResult, screen } from '@testing-library/react'
import Promise from 'bluebird'
import _ from 'lodash'
import withData, { Config, WithData } from './withData'
import ErrorReporterContext, {
  ErrorReporter,
  ConsoleErrorReporter,
} from 'ErrorReporterContext'
import { silenceConsoleErrors } from 'setupTests'

interface Props {
  prop: string
}

const data = 'Test data'
const defaultData = 'Default data'
const newData = 'New Test Data'
const propValue = 'passed value'
const newPropValue = 'new value'
const errorMessage = 'error'

let filter: jest.Mock<boolean, [Props]>
let config: Config<Props, string>
let getter: jest.Mock<Promise<string>, [Props]>
let ComponentWithData: React.ComponentType<Props>
let InnerComponent: jest.Mock<JSX.Element, [WithData<Props, string>]>

const errorReportingService: ErrorReporter = new ConsoleErrorReporter()

function renderWithData(): RenderResult {
  return render(
    <ErrorReporterContext.Provider value={errorReportingService}>
      <ComponentWithData prop={propValue} />{' '}
    </ErrorReporterContext.Provider>
  )
}

function clearMocks(): void {
  InnerComponent.mockClear()
  getter.mockClear()
}

function expectGetterToBeCalled(expectedProp: string): void {
  it('Calls getter with props', () => {
    expect(getter).toBeCalledWith({ prop: expectedProp })
  })
}

function expectWrappedComponentToBeRendered(
  expectedPropValue: string,
  expectedData: string
): void {
  it('Renders the wrapped component', () => {
    expect(
      screen.getByText(`${expectedPropValue} ${expectedData}`)
    ).toBeInTheDocument()
  })

  it('Passes properties to inner component', () => {
    expect(InnerComponent).toHaveBeenCalledWith(
      {
        data: expectedData,
        prop: expectedPropValue,
      },
      {}
    )
  })
}

beforeEach(async () => {
  const watch = (props: Props): [string] => [props.prop]
  filter = jest.fn()
  filter.mockReturnValue(true)
  getter = jest.fn()
  InnerComponent = jest.fn()
  InnerComponent.mockImplementation((props: WithData<Props, string>) => (
    <h1>
      {props.prop} {props.data}
    </h1>
  ))
  config = {
    watch,
    filter,
    defaultData: () => defaultData,
  }
  ComponentWithData = withData<Props, unknown, string>(
    InnerComponent,
    getter,
    config
  )
})

describe('On successful get', () => {
  let rerender: (
    ui: React.ReactElement<any, string | React.JSXElementConstructor<any>>
  ) => void

  function rerenderView(prop: string): void {
    rerender(
      <ErrorReporterContext.Provider value={errorReportingService}>
        <ComponentWithData prop={prop} />{' '}
      </ErrorReporterContext.Provider>
    )
  }

  beforeEach(async () => {
    getter.mockReturnValueOnce(Promise.resolve(data))
    rerender = renderWithData().rerender
    await screen.findByText(RegExp(propValue))
  })

  expectGetterToBeCalled(propValue)
  expectWrappedComponentToBeRendered(propValue, data)

  describe('When updating', () => {
    beforeEach(async () => {
      clearMocks()
      getter.mockReturnValueOnce(Promise.resolve(newData))
    })

    describe('Prop updated', () => {
      beforeEach(async () => {
        rerenderView(newPropValue)
        await screen.findByText(RegExp(newPropValue))
      })

      expectGetterToBeCalled(newPropValue)
      expectWrappedComponentToBeRendered(newPropValue, newData)
    })

    describe('Prop did not update', () => {
      beforeEach(() => {
        rerenderView(propValue)
      })

      it('Does not query the API', () => {
        expect(getter).not.toHaveBeenCalled()
      })

      it('Does not rerender inner component', () => {
        expect(InnerComponent).not.toHaveBeenCalledWith()
      })
    })
  })
})

describe('On failed request', () => {
  beforeEach(async () => {
    getter.mockImplementationOnce(() => Promise.reject(new Error(errorMessage)))
    renderWithData()
    await screen.findByText(errorMessage)
  })

  it('Does not render wrapped component', () => {
    expect(InnerComponent).not.toHaveBeenCalled()
  })
})

describe('When unmounting', () => {
  let promise: Promise<string>

  beforeEach(async () => {
    promise = new Promise(_.noop)
    getter.mockReturnValueOnce(promise)
    const { unmount } = renderWithData()
    unmount()
  })

  it('Cancels the promise', () => {
    expect(promise.isCancelled()).toBe(true)
  })

  it('Does not show error', () => {
    expect(screen.queryByText(errorMessage)).not.toBeInTheDocument()
  })

  it('Does not render wrapped component', () => {
    expect(InnerComponent).not.toHaveBeenCalled()
  })
})

describe('Filtering', () => {
  beforeEach(async () => {
    filter.mockReturnValueOnce(false)
    renderWithData()
  })

  it('Calls the filter with props', () => {
    expect(filter).toHaveBeenCalledWith({
      prop: propValue,
    })
  })

  it('Does not query the API', () => {
    expect(getter).not.toHaveBeenCalled()
  })

  expectWrappedComponentToBeRendered(propValue, defaultData)
})

describe('Child component crash', () => {
  it('Displays error message', async () => {
    silenceConsoleErrors()
    const CrashingComponent = withData<unknown, unknown, string>(
      () => {
        throw new Error(errorMessage)
      },
      () => Promise.resolve(data)
    )
    render(
      <ErrorReporterContext.Provider value={errorReportingService}>
        <CrashingComponent />
      </ErrorReporterContext.Provider>
    )
    await screen.findByText("Something's gone wrong.")
  })
})