~statianzo/angularjs-testing-library

170666561972531f131342858cb93674845c663e — Jason Staten 4 years ago ace9f5b
feat: wait flushes angular tasks
4 files changed, 85 insertions(+), 99 deletions(-)

M src/__tests__/end-to-end.js
D src/flush-microtasks.js
M src/index.js
M src/pure.js
M src/__tests__/end-to-end.js => src/__tests__/end-to-end.js +47 -26
@@ 1,39 1,60 @@
import React from 'react'
import angular from 'angular'
import 'angular-mocks'
import {render, wait} from '../'

const fetchAMessage = () =>
  new Promise(resolve => {
    // we are using random timeout here to simulate a real-time example
    // of an async operation calling a callback at a non-deterministic time
    const randomTimeout = Math.floor(Math.random() * 100)
    setTimeout(() => {
      resolve({returnedMessage: 'Hello World'})
    }, randomTimeout)
  })
class controller {
  loading = true
  data = null

  constructor($q, $timeout) {
    this.$q = $q
    this.$timeout = $timeout
  }

class ComponentWithLoader extends React.Component {
  state = {loading: true}
  async componentDidMount() {
    const data = await fetchAMessage()
    this.setState({data, loading: false}) // eslint-disable-line
  load() {
    return this.$q(resolve => {
      // we are using random timeout here to simulate a real-time example
      // of an async operation calling a callback at a non-deterministic time
      const randomTimeout = Math.floor(Math.random() * 100)
      this.$timeout(() => {
        resolve({returnedMessage: 'Hello World'})
      }, randomTimeout)
    })
  }
  render() {
    if (this.state.loading) {
      return <div>Loading...</div>
    }
    return (
      <div data-testid="message">
        Loaded this message: {this.state.data.returnedMessage}!
      </div>
    )

  $onInit() {
    this.load().then(response => {
      this.loading = false
      this.data = response
    })
  }
}

const template = `
  <div ng-if="$ctrl.loading">Loading...</div>
  <div
    ng-if="!$ctrl.loading"
    data-testid="message"
  >
    Loaded this message: {{$ctrl.data.returnedMessage}}!
  </div>
`

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

test('it waits for the data to be loaded', async () => {
  const {queryByText, queryByTestId} = render(<ComponentWithLoader />)
  const {queryByText, findByTestId} = render(`<atl-end></atl-end>`)

  expect(queryByText('Loading...')).toBeTruthy()

  await wait(() => expect(queryByText('Loading...')).toBeNull())
  expect(queryByTestId('message').textContent).toMatch(/Hello World/)
  const message = await findByTestId('message')
  expect(message.textContent).toMatch(/Hello World/)
})

D src/flush-microtasks.js => src/flush-microtasks.js +0 -47
@@ 1,47 0,0 @@
/* istanbul ignore file */
// the part of this file that we need tested is definitely being run
// and the part that is not cannot easily have useful tests written
// anyway. So we're just going to ignore coverage for this file
/**
 * copied from React's enqueueTask.js
 */

let didWarnAboutMessageChannel = false
let enqueueTask
try {
  // read require off the module object to get around the bundlers.
  // we don't want them to detect a require and bundle a Node polyfill.
  const requireString = `require${Math.random()}`.slice(0, 7)
  const nodeRequire = module && module[requireString]
  // assuming we're in node, let's try to get node's
  // version of setImmediate, bypassing fake timers if any.
  enqueueTask = nodeRequire('timers').setImmediate
} catch (_err) {
  // we're in a browser
  // we can't use regular timers because they may still be faked
  // so we try MessageChannel+postMessage instead
  enqueueTask = callback => {
    if (didWarnAboutMessageChannel === false) {
      didWarnAboutMessageChannel = true
      // eslint-disable-next-line no-console
      console.error(
        typeof MessageChannel !== 'undefined',
        'This browser does not have a MessageChannel implementation, ' +
          'so enqueuing tasks via await act(async () => ...) will fail. ' +
          'Please file an issue at https://github.com/facebook/react/issues ' +
          'if you encounter this warning.',
      )
    }
    const channel = new MessageChannel()
    channel.port1.onmessage = callback
    channel.port2.postMessage(undefined)
  }
}

export default function flushMicroTasks() {
  return {
    then(resolve) {
      enqueueTask(resolve)
    },
  }
}

M src/index.js => src/index.js +3 -4
@@ 1,5 1,4 @@
import flush from './flush-microtasks'
import {cleanup} from './pure'
import {flush, cleanup} from './pure'

// if we're running in a test runner that supports afterEach
// then we'll automatically run cleanup afterEach test


@@ 7,8 6,8 @@ import {cleanup} from './pure'
// if you don't like this then either import the `pure` module
// or set the ATL_SKIP_AUTO_CLEANUP env variable to 'true'.
if (typeof afterEach === 'function' && !process.env.ATL_SKIP_AUTO_CLEANUP) {
  afterEach(async () => {
    await flush()
  afterEach(() => {
    flush()
    cleanup()
  })
}

M src/pure.js => src/pure.js +35 -22
@@ 4,6 4,7 @@ import {
  getQueriesForElement,
  prettyDOM,
  fireEvent as dtlFireEvent,
  wait as dtlWait,
} from '@testing-library/dom'

const mountedContainers = new Set()


@@ 24,21 25,17 @@ function render(ui, {container, baseElement = container, queries, scope} = {}) {
  // they're passing us a custom container or not.
  mountedContainers.add(container)

  let $scope
  angular.mock.inject([
    '$compile',
    '$rootScope',
    ($compile, $rootScope) => {
      $scope = $rootScope.$new()
      mountedScopes.add($scope)
      Object.assign($scope, scope)
  const $rootScope = getAngularService('$rootScope')
  const $compile = getAngularService('$compile')
  const $scope = $rootScope.$new()
  Object.assign($scope, scope)

      const element = $compile(ui)($scope)[0]
      container.appendChild(element)
  mountedScopes.add($scope)

      $scope.$digest()
    },
  ])
  const element = $compile(ui)($scope)[0]
  container.appendChild(element)

  $scope.$digest()

  return {
    container,


@@ 84,19 81,19 @@ function cleanupScope(scope) {
  scope.$destroy()
}

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

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


@@ 104,7 101,7 @@ function fireEvent(...args) {

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


@@ 115,5 112,21 @@ Object.keys(dtlFireEvent).forEach(key => {
fireEvent.mouseEnter = fireEvent.mouseOver
fireEvent.mouseLeave = fireEvent.mouseOut

function flush() {
  const $browser = getAngularService('$browser')
  if ($browser.deferredFns.length) {
    $browser.defer.flush()
  }
}

function wait(callback, options) {
  dtlWait(() => {
    flush()
    if (callback) {
      callback()
    }
  }, options)
}

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