title: Better way to e2e test Toast animations with detox
body: <p>I'm trying to test the following Toast component:</p>
  
  <pre><code>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 &gt; 0) {
          dismissTimeout = setTimeout(() =&gt; {
            this.props.hideToast()
          }, duration)
        }
  
        clearTimeout(this.state.dismissTimeout)
        this.show(message, { error, warning, dismissTimeout, duration })
      } else {
        this.state.dismissTimeout &amp;&amp; clearTimeout(this.state.dismissTimeout)
        this.hide()
      }
    }
  
    show(message, { error, warning, dismissTimeout, duration }) {
      if (Platform.OS === "android") {
        const androidDuration =
          duration &lt; 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,
          },
          () =&gt; {
            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(() =&gt; {
          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 (
        &lt;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 =&gt; this.setState({})}
        &gt;
          &lt;TouchableOpacity
            onPress={this.dispatchHide.bind(this)}
            activeOpacity={1}
          &gt;
            &lt;View style={styles.messageWrapper}&gt;
              &lt;View
                testID={"toast"}
                style={messageStyles}
                onLayout={evt =&gt; {
                  this.setState({
                    width: evt.nativeEvent.layout.width,
                    height: evt.nativeEvent.layout.height,
                  })
                }}
              &gt;
                {this.state.dismissTimeout === null ? (
                  &lt;TouchableOpacity
                    style={{ alignItems: "flex-end" }}
                    onPress={this.dispatchHide.bind(this)}
                  &gt;
                    &lt;IconFe name={"x"} color={"white"} size={16} /&gt;
                  &lt;/TouchableOpacity&gt;
                ) : null}
                {this.props.getMessageComponent(this.state.message, {
                  error: this.state.error,
                  warning: this.state.warning,
                })}
              &lt;/View&gt;
            &lt;/View&gt;
          &lt;/TouchableOpacity&gt;
        &lt;/Animated.View&gt;
      )
    }
  
    render() {
      if (Platform.OS === "ios") {
        return this._renderIOS()
      } else {
        return null
      }
    }
  }
  
  const styles = RkStyleSheet.create(theme =&gt; {
    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 &lt;RkText style={styles.messageStyle}&gt;{message}&lt;/RkText&gt;
    },
    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
  </code></pre>
  
  <p>Running this on iOS outputs a View with a Text message. My view has a testID set to "toast". To show the toast we dispatch a redux action, which in term triggers the Toast.</p>
  
  <p>I have the following test that fails: </p>
  
  <pre><code>    it("submit without username should display invalid username", async () =&gt; {
        await element(by.id("letsGo")).tap()
        await expect(element(by.id("toast"))).toBeVisible()
      });
  </code></pre>
  
  <p>I understand that the test fails because of the automatic synchronization (<a href="https://github.com/wix/Detox/blob/master/docs/Troubleshooting.Synchronization.md" rel="nofollow noreferrer">https://github.com/wix/Detox/blob/master/docs/Troubleshooting.Synchronization.md</a>) of detox. When we press the button we dispatch a redux action. The toast displays and a setTimeout of 4s is set. Now detox waits 4s before it tests whether the "toast"element is visible or not. When the 4s are over the element is destroyed from the view and detox cannot find it. </p>
  
  <p>There are different workarounds for this. The first one would be to disableSynchronization before taping on the button and then to enable it after the toast has been displayed. this works, but the test needs 4s+ to complete. For some reason even though the sync is disabled we still wait for the setTimeout to complete, but this time we see the element.</p>
  
  <pre><code>    it("submit without username should display invalid username", async () =&gt; {
        await device.disableSynchronization();
        await element(by.id("letsGo")).tap()
        await waitFor(element(by.id("toastWTF"))).toBeVisible().withTimeout(1000)
        await device.enableSynchronization();
      });
  </code></pre>
  
  <p>Another option as per the docs is to disable the animation for e2e tests. I tested this and it's working, but I'm wondering if there is a better way?</p>
  
  <p>In this particular case the actual animation takes few hundred ms and after that we display the view and wait for it to disapper. There is no need for detox to wait. The real users using the app also don't have to wait either. </p>
  
  <p>Is there any way to make this whole thing a little more user friendly for people writing the tests :)</p>