更好的e2e排毒测试Toast动画的方法

问题描述 投票:0回答:1

我正在尝试测试以下Toast组件:

import React, { Component } from "react"
import PropTypes from "prop-types"
import {
  Animated,
  Platform,
  Text,
  ToastAndroid,
  TouchableOpacity,
  View,
} from "react-native"
import { RkStyleSheet, RkText } from "react-native-ui-kitten"
import IconFe from "react-native-vector-icons/Feather"
import { UIConstants } from "constants/appConstants"

class Toast extends Component {
  constructor(props) {
    super(props)
    this.state = {
      fadeAnimation: new Animated.Value(0),
      shadowOpacity: new Animated.Value(0),
      timeLeftAnimation: new Animated.Value(0),
      present: false,
      message: "",
      dismissTimeout: null,
      height: 0,
      width: 0,
    }
  }

  /* eslint-disable-next-line  */
  UNSAFE_componentWillReceiveProps(
    { message, error, duration, warning },
    ...rest
  ) {
    if (message) {
      let dismissTimeout = null
      if (duration > 0) {
        dismissTimeout = setTimeout(() => {
          this.props.hideToast()
        }, duration)
      }

      clearTimeout(this.state.dismissTimeout)
      this.show(message, { error, warning, dismissTimeout, duration })
    } else {
      this.state.dismissTimeout && clearTimeout(this.state.dismissTimeout)
      this.hide()
    }
  }

  show(message, { error, warning, dismissTimeout, duration }) {
    if (Platform.OS === "android") {
      const androidDuration =
        duration < 3000 ? ToastAndroid.SHORT : ToastAndroid.LONG
      ToastAndroid.showWithGravityAndOffset(
        message,
        androidDuration,
        ToastAndroid.TOP,
        0,
        UIConstants.HeaderHeight
      )
    } else {
      this.setState(
        {
          present: true,
          fadeAnimation: new Animated.Value(0),
          shadowOpacity: new Animated.Value(0),
          timeLeftAnimation: new Animated.Value(0),
          message,
          error,
          warning,
          dismissTimeout,
        },
        () => {
          Animated.spring(this.state.fadeAnimation, {
            toValue: 1,
            friction: 4,
            tension: 40,
          }).start()
          Animated.timing(this.state.shadowOpacity, { toValue: 0.5 }).start()
          Animated.timing(this.state.timeLeftAnimation, {
            duration,
            toValue: 1,
          }).start()
        }
      )
    }
  }

  hide() {
    if (Platform.OS === "ios") {
      Animated.timing(this.state.shadowOpacity, { toValue: 0 }).start()
      Animated.spring(this.state.fadeAnimation, { toValue: 0 }).start(() => {
        this.setState({
          present: false,
          message: null,
          error: false,
          warning: false,
          dismissTimeout: null,
        })
      })
    }
  }

  dispatchHide() {
    this.props.hideToast()
  }

  _renderIOS() {
    if (!this.state.present) {
      return null
    }

    const messageStyles = [styles.messageContainer, this.props.containerStyle]
    if (this.state.error) {
      messageStyles.push(styles.error, this.props.errorStyle)
    } else if (this.state.warning) {
      messageStyles.push(styles.warning, this.props.warningStyle)
    }

    return (
      <Animated.View
        style={[
          styles.container,
          {
            opacity: this.state.fadeAnimation,
            transform: [
              {
                translateY: this.state.fadeAnimation.interpolate({
                  inputRange: [0, 1],
                  outputRange: [0, this.state.height], // 0 : 150, 0.5 : 75, 1 : 0
                }),
              },
            ],
          },
        ]}
        onLayout={evt => this.setState({})}
      >
        <TouchableOpacity
          onPress={this.dispatchHide.bind(this)}
          activeOpacity={1}
        >
          <View style={styles.messageWrapper}>
            <View
              testID={"toast"}
              style={messageStyles}
              onLayout={evt => {
                this.setState({
                  width: evt.nativeEvent.layout.width,
                  height: evt.nativeEvent.layout.height,
                })
              }}
            >
              {this.state.dismissTimeout === null ? (
                <TouchableOpacity
                  style={{ alignItems: "flex-end" }}
                  onPress={this.dispatchHide.bind(this)}
                >
                  <IconFe name={"x"} color={"white"} size={16} />
                </TouchableOpacity>
              ) : null}
              {this.props.getMessageComponent(this.state.message, {
                error: this.state.error,
                warning: this.state.warning,
              })}
            </View>
          </View>
        </TouchableOpacity>
      </Animated.View>
    )
  }

  render() {
    if (Platform.OS === "ios") {
      return this._renderIOS()
    } else {
      return null
    }
  }
}

const styles = RkStyleSheet.create(theme => {
  return {
    container: {
      zIndex: 10000,
      position: "absolute",
      left: 0,
      right: 0,
      top: 10,
    },
    messageWrapper: {
      justifyContent: "center",
      alignItems: "center",
    },
    messageContainer: {
      paddingHorizontal: 15,
      paddingVertical: 15,
      borderRadius: 15,
      backgroundColor: "rgba(238,238,238,0.9)",
    },
    messageStyle: {
      color: theme.colors.black,
      fontSize: theme.fonts.sizes.small,
    },
    timeLeft: {
      height: 2,
      backgroundColor: theme.colors.primary,
      top: 2,
      zIndex: 10,
    },
    error: {
      backgroundColor: "red",
    },
    warning: {
      backgroundColor: "yellow",
    },
  }
})

Toast.defaultProps = {
  getMessageComponent(message) {
    return <RkText style={styles.messageStyle}>{message}</RkText>
  },
  duration: 5000,
}

Toast.propTypes = {
  // containerStyle: View.propTypes.style,
  message: PropTypes.string,
  messageStyle: Text.propTypes.style, // eslint-disable-line react/no-unused-prop-types
  error: PropTypes.bool,
  // errorStyle: View.propTypes.style,
  warning: PropTypes.bool,
  // warningStyle: View.propTypes.style,
  duration: PropTypes.number,
  getMessageComponent: PropTypes.func,
}

export default Toast

在iOS上运行此命令会输出带有文本消息的视图。我的视图的testID设置为“ toast”。为了显示吐司,我们调度了一个redux动作,该动作会触发吐司。

我有以下测试失败:

    it("submit without username should display invalid username", async () => {
      await element(by.id("letsGo")).tap()
      await expect(element(by.id("toast"))).toBeVisible()
    });

我知道测试由于排毒的自动同步(https://github.com/wix/Detox/blob/master/docs/Troubleshooting.Synchronization.md)而失败。当我们按下按钮时,我们调度一个redux动作。显示吐司,并且将setTimeout设置为4s。现在排毒要等待4秒钟,然后才能测试“吐司”元素是否可见。当4结束时,该元素将从视图中销毁,并且排毒无法找到它。

有不同的解决方法。第一个是在点击按钮之前禁用Synchronization,然后在显示Toast之后启用它。此方法有效,但测试需要4s +才能完成。由于某些原因,即使同步被禁用,我们仍然等待setTimeout完成,但是这次我们看到了元素。

    it("submit without username should display invalid username", async () => {
      await device.disableSynchronization();
      await element(by.id("letsGo")).tap()
      await waitFor(element(by.id("toastWTF"))).toBeVisible().withTimeout(1000)
      await device.enableSynchronization();
    });

根据文档的另一个选项是为e2e测试禁用动画。我对此进行了测试,并且可以正常工作,但是我想知道是否有更好的方法?

在这种情况下,实际的动画需要几百毫秒,之后我们显示视图并等待其消失。无需排毒等待。使用该应用程序的真实用户也不必等待。

有什么方法可以使整个事情对编写测试的人更友好:)

react-native detox
1个回答
0
投票

Leo Natan是正确的。发生了其他事情。不知道确切是什么,但是在重写Toast组件以不使用componentWillReceiveProps之后,我能够等待它出现而没有指定超时。

© www.soinside.com 2019 - 2024. All rights reserved.