使用 detox 对 Toast 动画进行 e2e 测试的更好方法
Posted
技术标签:
【中文标题】使用 detox 对 Toast 动画进行 e2e 测试的更好方法【英文标题】:Better way to e2e test Toast animations with detox 【发布时间】:2020-03-06 23:06:38 【问题描述】:我正在尝试测试以下 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”。为了显示 toast,我们调度了一个 redux 操作,它在术语上触发了 Toast。
我有以下测试失败:
it("submit without username should display invalid username", async () =>
await element(by.id("letsGo")).tap()
await expect(element(by.id("toast"))).toBeVisible()
);
我了解测试失败是因为 detox 的自动同步 (https://github.com/wix/Detox/blob/master/docs/Troubleshooting.Synchronization.md)。当我们按下按钮时,我们会发送一个 redux 动作。吐司显示并设置了 4s 的 setTimeout。现在 detox 在测试“toast”元素是否可见之前等待 4 秒。当 4s 结束时,元素从视图中被破坏并且 detox 找不到它。
对此有不同的解决方法。第一个是在点击按钮之前禁用同步,然后在显示吐司后启用它。这可行,但测试需要 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 测试的动画。我对此进行了测试并且它正在工作,但我想知道是否有更好的方法?
在这种特殊情况下,实际动画需要几百毫秒,然后我们显示视图并等待它消失。排毒无需等待。使用该应用程序的真实用户也不必等待。
有什么方法可以让整个事情对编写测试的人更加用户友好:)
【问题讨论】:
排毒不会等待超过 1.5 秒的计时器。 所以这里:dismissTimeout = setTimeout(() => this.props.hideToast() , 4000) 将在 1.5 秒后被忽略??? 从一开始就会被忽略。任何在 1.5 秒后触发的计时器都会被忽略。 好的,那么这没有意义。如果排毒忽略计时器,为什么我的测试要等待 4 秒才能完成?它不应该立即完成测试吗? github.com/wix/Detox/blob/… 其他东西让 Detox 等待。 【参考方案1】:Leo Natan 是对的。其他事情正在发生。不确定它到底是什么,但是在重写我的 Toast 组件以不使用 componentWillReceiveProps 之后,我能够等待它出现而不指定超时。
【讨论】:
以上是关于使用 detox 对 Toast 动画进行 e2e 测试的更好方法的主要内容,如果未能解决你的问题,请参考以下文章
在 e2e 测试中的某个点开始使用 Detox 在 react native
使用 detox 测试 e2e 无法启动 iPhone 模拟器,而是启动 Apple TV
Detox E2E 测试不会在 react-native 项目上运行
React Native E2E w/Detox 错误:“无法在设备上运行应用程序”