Add E2E testing infrastructure with WebdriverIO + Appium

Set up E2E test framework for Android using WebdriverIO, Appium, and
UiAutomator2. Add testID props to key components (AuthButton, BaseButton,
ChatBubble, CustomTextInput, ProposedEventCard) and apply testIDs to
login screen, chat screen, tab bar, and settings. Include initial tests
for app launch detection and login flow validation. Update CLAUDE.md
with E2E docs.
This commit is contained in:
2026-02-26 21:37:40 +01:00
parent 4f5737d27e
commit 27602aee4c
21 changed files with 12549 additions and 41 deletions

View File

@@ -19,6 +19,7 @@ export default function TabLayout() {
name="chat"
options={{
title: "Chat",
tabBarTestID: "tab-chat",
tabBarIcon: ({ color }) => (
<Ionicons size={28} name="chatbubble" color={color} />
),
@@ -28,6 +29,7 @@ export default function TabLayout() {
name="calendar"
options={{
title: "Calendar",
tabBarTestID: "tab-calendar",
tabBarIcon: ({ color }) => (
<Ionicons size={28} name="calendar" color={color} />
),
@@ -37,6 +39,7 @@ export default function TabLayout() {
name="settings"
options={{
title: "Settings",
tabBarTestID: "tab-settings",
tabBarIcon: ({ color }) => (
<Ionicons size={28} name="settings" color={color} />
),

View File

@@ -342,6 +342,7 @@ const ChatInput = ({ onSend }: ChatInputProps) => {
return (
<View className="flex flex-row w-full items-end my-2 px-2">
<TextInput
testID="chat-message-input"
className="flex-1 border border-solid rounded-2xl px-3 py-2 mr-2"
style={{
backgroundColor: theme.messageBorderBg,
@@ -356,7 +357,7 @@ const ChatInput = ({ onSend }: ChatInputProps) => {
placeholderTextColor={theme.textMuted}
multiline
/>
<Pressable onPress={handleSend}>
<Pressable testID="chat-send-button" onPress={handleSend}>
<View
className="w-10 h-10 rounded-full items-center justify-center"
style={{
@@ -393,6 +394,7 @@ const ChatMessage = ({
return (
<ChatBubble
side={side}
testID={`chat-bubble-${side}`}
style={{
maxWidth: "80%",
minWidth: hasProposals ? "75%" : undefined,

View File

@@ -20,6 +20,7 @@ const handleLogout = async () => {
const SettingsButton = (props: BaseButtonProps) => {
return (
<BaseButton
testID={props.testID}
onPress={props.onPress}
solid={props.solid}
className={"w-11/12"}
@@ -214,7 +215,7 @@ const Settings = () => {
<BaseBackground>
<SimpleHeader text="Settings" />
<View className="flex items-center mt-4">
<SettingsButton onPress={handleLogout} solid={true}>
<SettingsButton testID="settings-logout-button" onPress={handleLogout} solid={true}>
<Ionicons name="log-out-outline" size={24} color={theme.primeFg} />{" "}
Logout
</SettingsButton>

View File

@@ -45,6 +45,7 @@ const LoginScreen = () => {
<BaseBackground>
<View className="flex-1 justify-center items-center p-8">
<Text
testID="login-title"
className="text-3xl font-bold mb-8"
style={{ color: theme.textPrimary }}
>
@@ -53,6 +54,7 @@ const LoginScreen = () => {
{error && (
<Text
testID="login-error-text"
className="mb-4 text-center"
style={{ color: theme.rejectButton }}
>
@@ -61,6 +63,7 @@ const LoginScreen = () => {
)}
<CustomTextInput
testID="login-identifier-input"
placeholder="E-Mail oder Benutzername"
placeholderTextColor={theme.textMuted}
text={identifier}
@@ -70,6 +73,7 @@ const LoginScreen = () => {
/>
<CustomTextInput
testID="login-password-input"
placeholder="Passwort"
placeholderTextColor={theme.textMuted}
text={password}
@@ -79,6 +83,7 @@ const LoginScreen = () => {
/>
<AuthButton
testID="login-button"
title="Anmelden"
onPress={handleLogin}
isLoading={isLoading}

View File

@@ -5,12 +5,14 @@ interface AuthButtonProps {
title: string;
onPress: () => void;
isLoading?: boolean;
testID?: string;
}
const AuthButton = ({ title, onPress, isLoading = false }: AuthButtonProps) => {
const AuthButton = ({ title, onPress, isLoading = false, testID }: AuthButtonProps) => {
const { theme } = useThemeStore();
return (
<Pressable
testID={testID}
onPress={onPress}
disabled={isLoading}
className="w-full rounded-lg p-4 mb-4 border-4"

View File

@@ -7,6 +7,7 @@ export type BaseButtonProps = {
className?: string;
onPress: () => void;
solid?: boolean;
testID?: string;
};
const BaseButton = ({
@@ -14,10 +15,12 @@ const BaseButton = ({
children,
onPress,
solid = false,
testID,
}: BaseButtonProps) => {
const { theme } = useThemeStore();
return (
<Pressable
testID={testID}
className={`rounded-lg p-4 mb-4 border-4 ${className}`}
onPress={onPress}
style={{

View File

@@ -8,6 +8,7 @@ type ChatBubbleProps = {
children: React.ReactNode;
className?: string;
style?: ViewStyle;
testID?: string;
};
export function ChatBubble({
@@ -15,6 +16,7 @@ export function ChatBubble({
children,
className = "",
style,
testID,
}: ChatBubbleProps) {
const { theme } = useThemeStore();
const borderColor = side === "left" ? theme.chatBot : theme.primeFg;
@@ -25,6 +27,7 @@ export function ChatBubble({
return (
<View
testID={testID}
className={`border-2 border-solid rounded-xl my-2 ${sideClass} ${className}`}
style={[
{ borderColor, elevation: 8, backgroundColor: theme.secondaryBg },

View File

@@ -13,6 +13,7 @@ export type CustomTextInputProps = {
secureTextEntry?: boolean;
autoCapitalize?: TextInputProps["autoCapitalize"];
keyboardType?: TextInputProps["keyboardType"];
testID?: string;
};
const CustomTextInput = (props: CustomTextInputProps) => {
@@ -21,6 +22,7 @@ const CustomTextInput = (props: CustomTextInputProps) => {
return (
<TextInput
testID={props.testID}
className={`border border-solid rounded-2xl ${props.className}`}
onChangeText={props.onValueChange}
value={props.text}

View File

@@ -31,6 +31,7 @@ const ActionButtons = ({
return (
<View className="flex-row mt-3 gap-2">
<Pressable
testID="event-accept-button"
onPress={onConfirm}
disabled={isDisabled}
className="flex-1 py-2 rounded-lg items-center"
@@ -47,6 +48,7 @@ const ActionButtons = ({
</Text>
</Pressable>
<Pressable
testID="event-reject-button"
onPress={onReject}
disabled={isDisabled}
className="flex-1 py-2 rounded-lg items-center"