~statianzo/angularjs-testing-library

ace9f5bdbf166424d127e8aa09c09c62bacd77bc — Jason Staten 4 years ago b69df8d
feat(events): fireEvent triggers a digest
4 files changed, 68 insertions(+), 155 deletions(-)

A src/__tests__/digest.js
M src/__tests__/events.js
D src/act-compat.js
M src/pure.js
A src/__tests__/digest.js => src/__tests__/digest.js +39 -0
@@ 0,0 1,39 @@
import angular from 'angular'
import 'angular-mocks'
import {render, fireEvent} from '../'

beforeEach(() => {
  angular.module('atl', [])
  angular.mock.module('atl')
})

test('`fireEvent` triggers a digest', () => {
  angular.module('atl').component('atlDigest', {
    template: `
      <button ng-ref="$ctrl.btn">
        Click Me
      </button>
      <div ng-if="$ctrl.wasClicked">
        Clicked!
      </div>
    `,
    controller: class {
      wasClicked = false
      btn = null
      $postLink() {
        this.btn.on('click', this.handleClick)
      }

      handleClick = () => {
        this.wasClicked = true
      }
    },
  })

  const {getByRole, queryByText} = render(`<atl-digest></atl-digest>`)

  const button = getByRole('button')
  expect(queryByText('Clicked!')).toBeNull()
  fireEvent.click(button)
  expect(queryByText('Clicked!')).not.toBeNull()
})

M src/__tests__/events.js => src/__tests__/events.js +7 -11
@@ 135,10 135,8 @@ eventTypes.forEach(({type, events, elementType, init}) => {
      it(`triggers ${eventName}`, () => {
        const spy = jest.fn()

        const {getByTestId} = render(
          `
          <${elementType}
            data-testid="target"
        const {container} = render(
          `<${elementType}
            ng-on-${propName}="spy()"
          ></${elementType}>`,
          {


@@ 148,8 146,7 @@ eventTypes.forEach(({type, events, elementType, init}) => {
          },
        )

        const target = getByTestId('target')
        fireEvent[eventName](target, init)
        fireEvent[eventName](container.firstChild, init)
        expect(spy).toHaveBeenCalledTimes(1)
      })
    })


@@ 160,11 157,10 @@ test('calling `fireEvent` directly works too', () => {
  const spy = jest.fn()

  const {getByTestId} = render(
    `
          <button
            data-testid="target"
            ng-click="spy()"
          ></button>`,
    `<button
      data-testid="target"
      ng-click="spy()"
    ></button>`,
    {
      scope: {
        spy,

D src/act-compat.js => src/act-compat.js +0 -135
@@ 1,135 0,0 @@
import React from 'react'
import ReactDOM from 'react-dom'
import * as testUtils from 'react-dom/test-utils'

const reactAct = testUtils.act
const actSupported = reactAct !== undefined

// act is supported react-dom@16.8.0
// so for versions that don't have act from test utils
// we do this little polyfill. No warnings, but it's
// better than nothing.
function actPolyfill(cb) {
  ReactDOM.unstable_batchedUpdates(cb)
  ReactDOM.render(<div />, document.createElement('div'))
}

const act = reactAct || actPolyfill

let youHaveBeenWarned = false
let isAsyncActSupported = null

function asyncAct(cb) {
  if (actSupported === true) {
    if (isAsyncActSupported === null) {
      return new Promise((resolve, reject) => {
        // patch console.error here
        const originalConsoleError = console.error
        console.error = function error(...args) {
          /* if console.error fired *with that specific message* */
          /* istanbul ignore next */
          const firstArgIsString = typeof args[0] === 'string'
          if (
            firstArgIsString &&
            args[0].indexOf(
              'Warning: Do not await the result of calling ReactTestUtils.act',
            ) === 0
          ) {
            // v16.8.6
            isAsyncActSupported = false
          } else if (
            firstArgIsString &&
            args[0].indexOf(
              'Warning: The callback passed to ReactTestUtils.act(...) function must not return anything',
            ) === 0
          ) {
            // no-op
          } else {
            originalConsoleError.apply(console, args)
          }
        }
        let cbReturn, result
        try {
          result = reactAct(() => {
            cbReturn = cb()
            return cbReturn
          })
        } catch (err) {
          console.error = originalConsoleError
          reject(err)
          return
        }

        result.then(
          () => {
            console.error = originalConsoleError
            // if it got here, it means async act is supported
            isAsyncActSupported = true
            resolve()
          },
          err => {
            console.error = originalConsoleError
            isAsyncActSupported = true
            reject(err)
          },
        )

        // 16.8.6's act().then() doesn't call a resolve handler, so we need to manually flush here, sigh

        if (isAsyncActSupported === false) {
          console.error = originalConsoleError
          /* istanbul ignore next */
          if (!youHaveBeenWarned) {
            // if act is supported and async act isn't and they're trying to use async
            // act, then they need to upgrade from 16.8 to 16.9.
            // This is a seemless upgrade, so we'll add a warning
            console.error(
              `It looks like you're using a version of react-dom that supports the "act" function, but not an awaitable version of "act" which you will need. Please upgrade to at least react-dom@16.9.0 to remove this warning.`,
            )
            youHaveBeenWarned = true
          }

          cbReturn.then(() => {
            // a faux-version.
            // todo - copy https://github.com/facebook/react/blob/master/packages/shared/enqueueTask.js
            Promise.resolve().then(() => {
              // use sync act to flush effects
              act(() => {})
              resolve()
            })
          }, reject)
        }
      })
    } else if (isAsyncActSupported === false) {
      // use the polyfill directly
      let result
      act(() => {
        result = cb()
      })
      return result.then(() => {
        return Promise.resolve().then(() => {
          // use sync act to flush effects
          act(() => {})
        })
      })
    }
    // all good! regular act
    return act(cb)
  }
  // use the polyfill
  let result
  act(() => {
    result = cb()
  })
  return result.then(() => {
    return Promise.resolve().then(() => {
      // use sync act to flush effects
      act(() => {})
    })
  })
}

export default act
export {asyncAct}

/* eslint no-console:0 */

M src/pure.js => src/pure.js +22 -9
@@ 84,23 84,36 @@ function cleanupScope(scope) {
  scope.$destroy()
}

function getRootScope() {
  let $rootScope
  angular.mock.inject([
    '$rootScope',
    rootScope => {
      $rootScope = rootScope
    },
  ])
  return $rootScope
}

function fireEvent(...args) {
  return dtlFireEvent(...args)
  const $rootScope = getRootScope()
  const result = dtlFireEvent(...args)
  $rootScope.$digest()
  return result
}

Object.keys(dtlFireEvent).forEach(key => {
  fireEvent[key] = (...args) => {
    return dtlFireEvent[key](...args)
    const $rootScope = getRootScope()
    const result = dtlFireEvent[key](...args)
    $rootScope.$digest()
    return result
  }
})

fireEvent.mouseEnter = (...args) => {
  return dtlFireEvent.mouseOver(...args)
}

fireEvent.mouseLeave = (...args) => {
  return dtlFireEvent.mouseOut(...args)
}
// AngularJS maps `mouseEnter` to `mouseOver` and `mouseLeave` to `mouseOut`
fireEvent.mouseEnter = fireEvent.mouseOver
fireEvent.mouseLeave = fireEvent.mouseOut

export * from '@testing-library/dom'
export {render, cleanup, fireEvent}