Диагностика дублирующихся отчетов о спецификациях с помощью Karma, Mocha и React 16.5 - PullRequest
0 голосов
/ 07 сентября 2018

У меня есть проект, использующий React для слоя представления.Чтобы проверить это, я использую mocha, karma, karma-webpack и т. Д. По какой-то причине в React 16+ карма сообщает, что afterEach запускается три раза для двух спецификаций.Это происходит только в React 16+ и только , когда process.env.NODE_ENV равно development и не production.

В предыдущих исследованияхэтой проблемы причины (ы) сбоев спецификаций могут каскадироваться и загрязнять последующие спецификации.Чтобы помочь определить основную причину, это самый простой пример, который я могу найти.

Я пытался проследить поведение, но был озадачен сложностью внутри и вокруг кармы и сокетов.Рассмотрим приведенный ниже пример, доступный на данный момент на https://github.com/jneander/react-mocha.

Example.js

import React, {Component} from 'react'

export default class Example extends Component {
  render() {
    try {
      return (
        <div>{this.props.foo.bar}</div>
      )
    } catch(e) {
      console.log(e) // for logging purposes
      throw e
    }
  }
}

Example.spec.js

import {expect} from 'chai'
import React from 'react'
import ReactDOM from 'react-dom'

class ExampleWrapper extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      error: false
    }
  }

  componentDidCatch(error) {
    console.log('there was a problem')
    this.setState({
      error: true
    })
  }

  render() {
    console.log('rendering!')
    if (this.state.error) {
      console.log('- rendering the error version')
      return <div>An error occurred during render</div>
    }

    console.log('- rendering the real version')
    return (
      <Example {...this.props} />
    )
  }
}

import Example from './Example'

describe('Example', () => {
  let $container

  beforeEach(() => {
    console.log('beforeEach')
    $container = document.createElement('div')
    document.body.appendChild($container)
  })

  afterEach(() => {
    console.log('afterEach')
    ReactDOM.unmountComponentAtNode($container)
    $container.remove()
  })

  async function mount(props) {
    return new Promise((resolve, reject) => {
      const done = () => {
        console.log('done rendering')
        resolve()
      }
      ReactDOM.render(<ExampleWrapper {...props} />, $container, done)
    })
  }

  it('fails this spec', async () => {
    console.log('start test 1')
    await mount({})
    expect(true).to.be.true
  })

  it('also fails, but because of the first spec', async () => {
    console.log('start test 2')
    await mount({foo: {}})
    expect(true).to.be.true
  })
})

Ниже приведены технические характеристики:

LOG LOG: 'beforeEach'
LOG LOG: 'start test 1'
LOG LOG: 'rendering!'
LOG LOG: '- rendering the real version'

  Example
    ✗ fails this spec
  Error: Uncaught TypeError: Cannot read property 'bar' of undefined (src/Example.spec.js:35380)
      at Object.invokeGuardedCallbackDev (src/Example.spec.js:16547:16)
      at invokeGuardedCallback (src/Example.spec.js:16600:31)
      at replayUnitOfWork (src/Example.spec.js:31930:5)
      at renderRoot (src/Example.spec.js:32733:11)
      at performWorkOnRoot (src/Example.spec.js:33572:7)
      at performWork (src/Example.spec.js:33480:7)
      at performSyncWork (src/Example.spec.js:33452:3)
      at requestWork (src/Example.spec.js:33340:5)
      at scheduleWork (src/Example.spec.js:33134:5)

ERROR LOG: 'The above error occurred in the <Example> component:
    in Example (created by ExampleWrapper)
    in ExampleWrapper

React will try to recreate this component tree from scratch using the error boundary you provided, ExampleWrapper.'
LOG LOG: 'there was a problem'
LOG LOG: 'done rendering'
LOG LOG: 'rendering!'
LOG LOG: '- rendering the error version'
LOG LOG: 'afterEach'
LOG LOG: 'beforeEach'
LOG LOG: 'start test 2'
LOG LOG: 'rendering!'
LOG LOG: '- rendering the real version'
LOG LOG: 'done rendering'
    ✓ also fails, but because of the first spec
    ✓ also fails, but because of the first spec
LOG LOG: 'afterEach'
LOG LOG: 'afterEach'

Chrome 69.0.3497 (Mac OS X 10.13.6): Executed 3 of 2 (1 FAILED) (0.014 secs / NaN secs)
TOTAL: 1 FAILED, 2 SUCCESS


1) fails this spec
     Example
     Error: Uncaught TypeError: Cannot read property 'bar' of undefined (src/Example.spec.js:35380)
    at Object.invokeGuardedCallbackDev (src/Example.spec.js:16547:16)
    at invokeGuardedCallback (src/Example.spec.js:16600:31)
    at replayUnitOfWork (src/Example.spec.js:31930:5)
    at renderRoot (src/Example.spec.js:32733:11)
    at performWorkOnRoot (src/Example.spec.js:33572:7)
    at performWork (src/Example.spec.js:33480:7)
    at performSyncWork (src/Example.spec.js:33452:3)
    at requestWork (src/Example.spec.js:33340:5)
    at scheduleWork (src/Example.spec.js:33134:5)

Что вызывает дубликаты отчетов?

Почему это происходит в React 16+, а не в React 15?

Как мне решить эту проблему?

Ответы [ 2 ]

0 голосов
/ 18 сентября 2018

Похоже, что React 16+ выдает неперехваченную ошибку во время рендеринга, даже при использовании componentDidCatch в оболочке. Это означает, что Mocha не выполнит тест с неперехваченной ошибкой, а затем перейдет к следующему тесту, после которого второй рендеринг компонента завершит и разрешит обещание, выполнив утверждение. Это выполняется в рамках текущего теста, что приводит к двойному успеху, наблюдаемому в этом примере.

Была открыта проблема с репозиторием React на Github.

0 голосов
/ 07 сентября 2018

Могут быть условия гонки, потому что обещание разрешается с помощью функции ref. Ссылка на компонент получена не означает, что первоначальный рендеринг завершен.

Как ссылка состояний,

Если вам нужна ссылка на корневой экземпляр ReactComponent, предпочтительным решением является прикрепление ссылки обратного вызова к корневому элементу.

Правильный способ разрешения обещания - использовать render параметр обратного вызова,

Если предоставляется дополнительный обратный вызов, он будет выполнен после рендеринга или обновления компонента.

Должно быть:

async function mount(props) {
  return new Promise(resolve => {
    ReactDOM.render(<Example {...props} />, $container, resolve)
  })
}

Проблема не возникает во втором тесте, она возникает в первом тесте независимо от того, существует ли второй тест и не относится к React 16.5. Это зависит от того, как работает режим разработки React .

Вот упрощенная демонстрация , которая исключает фактор Мокка. Ожидаемые ошибки: console.warn, но две Error: Cannot read property 'bar' of undefined ошибки console.error, которые выводятся самой React. ReactDOM.render дважды запускает функцию компонента render и выдает ошибку из первого теста асинхронно.

То же самое демо с производственной сборкой React показывает единственную ошибку Error: Cannot read property 'bar' of undefined синхронно, как и следовало ожидать. Неудачный рендер не заставляет ReactDOM рендеринг отклонять, ошибка может быть перехвачена компонентом границы ошибки, если необходимо :

class EB extends Component {
  componentDidCatch(err) {
    this.props.onCatch(err);
  }

  render() {
    return this.props.children;
  }
}

async function mount(props) {
  return new Promise((resolve, reject) => {
    ReactDOM.render(<EB onCatch={reject}><Example {...props} /></EB>, $container, resolve)
  })
}

Хорошей практикой является не полагаться на рендерер React DOM в модульных тестах. Фермент служит для этой цели и позволяет синхронно тестировать компоненты, в частности shallow упаковщик .

...