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.
190 lines
5.7 KiB
TypeScript
190 lines
5.7 KiB
TypeScript
import { View, Text, Pressable } from "react-native";
|
|
import { Feather, Ionicons } from "@expo/vector-icons";
|
|
import { ProposedEventChange, formatDate, formatTime } from "@calchat/shared";
|
|
import { rrulestr } from "rrule";
|
|
import { useThemeStore } from "../stores/ThemeStore";
|
|
import { EventCardBase } from "./EventCardBase";
|
|
|
|
type ProposedEventCardProps = {
|
|
proposedChange: ProposedEventChange;
|
|
onConfirm: (proposal: ProposedEventChange) => void;
|
|
onReject: () => void;
|
|
onEdit?: (proposal: ProposedEventChange) => void;
|
|
};
|
|
|
|
const ActionButtons = ({
|
|
isDisabled,
|
|
respondedAction,
|
|
showEdit,
|
|
onConfirm,
|
|
onReject,
|
|
onEdit,
|
|
}: {
|
|
isDisabled: boolean;
|
|
respondedAction?: "confirm" | "reject";
|
|
showEdit: boolean;
|
|
onConfirm: () => void;
|
|
onReject: () => void;
|
|
onEdit?: () => void;
|
|
}) => {
|
|
const { theme } = useThemeStore();
|
|
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"
|
|
style={{
|
|
backgroundColor: isDisabled
|
|
? theme.disabledButton
|
|
: theme.confirmButton,
|
|
borderWidth: respondedAction === "confirm" ? 2 : 0,
|
|
borderColor: theme.confirmButton,
|
|
}}
|
|
>
|
|
<Text style={{ color: theme.buttonText }} className="font-medium">
|
|
Annehmen
|
|
</Text>
|
|
</Pressable>
|
|
<Pressable
|
|
testID="event-reject-button"
|
|
onPress={onReject}
|
|
disabled={isDisabled}
|
|
className="flex-1 py-2 rounded-lg items-center"
|
|
style={{
|
|
backgroundColor: isDisabled
|
|
? theme.disabledButton
|
|
: theme.rejectButton,
|
|
borderWidth: respondedAction === "reject" ? 2 : 0,
|
|
borderColor: theme.rejectButton,
|
|
}}
|
|
>
|
|
<Text style={{ color: theme.buttonText }} className="font-medium">
|
|
Ablehnen
|
|
</Text>
|
|
</Pressable>
|
|
{showEdit && onEdit && (
|
|
<Pressable
|
|
onPress={onEdit}
|
|
className="py-2 px-3 rounded-lg items-center"
|
|
style={{
|
|
backgroundColor: theme.secondaryBg,
|
|
borderWidth: 1,
|
|
borderColor: theme.borderPrimary,
|
|
}}
|
|
>
|
|
<Feather name="edit-2" size={18} color={theme.textPrimary} />
|
|
</Pressable>
|
|
)}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export const ProposedEventCard = ({
|
|
proposedChange,
|
|
onConfirm,
|
|
onReject,
|
|
onEdit,
|
|
}: ProposedEventCardProps) => {
|
|
const { theme } = useThemeStore();
|
|
const event = proposedChange.event;
|
|
const isDisabled = !!proposedChange.respondedAction;
|
|
|
|
// For delete/single action, the occurrenceDate becomes a new exception
|
|
const newExceptionDate =
|
|
proposedChange.action === "delete" &&
|
|
proposedChange.deleteMode === "single" &&
|
|
proposedChange.occurrenceDate;
|
|
|
|
// For update actions, check if a new UNTIL date is being set
|
|
const newUntilDate =
|
|
proposedChange.action === "update" &&
|
|
event?.recurrenceRule &&
|
|
rrulestr(event.recurrenceRule).options.until;
|
|
|
|
if (!event) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<View className="mt-2">
|
|
<EventCardBase
|
|
className="m-2"
|
|
title={event.title}
|
|
startTime={event.startTime}
|
|
endTime={event.endTime}
|
|
description={event.description}
|
|
recurrenceRule={event.recurrenceRule}
|
|
>
|
|
{/* Show new exception date for delete/single actions */}
|
|
{newExceptionDate && (
|
|
<View className="flex-row items-center mb-2">
|
|
<Feather
|
|
name="plus-circle"
|
|
size={16}
|
|
color={theme.confirmButton}
|
|
style={{ marginRight: 8 }}
|
|
/>
|
|
<Text
|
|
style={{ color: theme.confirmButton }}
|
|
className="font-medium"
|
|
>
|
|
Neue Ausnahme:{" "}
|
|
{formatDate(new Date(proposedChange.occurrenceDate!))}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
{/* Show new UNTIL date for update actions */}
|
|
{newUntilDate && (
|
|
<View className="flex-row items-center mb-2">
|
|
<Feather
|
|
name="plus-circle"
|
|
size={16}
|
|
color={theme.confirmButton}
|
|
style={{ marginRight: 8 }}
|
|
/>
|
|
<Text
|
|
style={{ color: theme.confirmButton }}
|
|
className="font-medium"
|
|
>
|
|
Neues Ende: {formatDate(newUntilDate)}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
{/* Show conflicting events warning */}
|
|
{proposedChange.conflictingEvents &&
|
|
proposedChange.conflictingEvents.length > 0 && (
|
|
<View className="mb-2">
|
|
{proposedChange.conflictingEvents.map((conflict, index) => (
|
|
<View key={index} className="flex-row items-center mt-1">
|
|
<Ionicons
|
|
name="alert-circle"
|
|
size={16}
|
|
color={theme.rejectButton}
|
|
style={{ marginRight: 8 }}
|
|
/>
|
|
<Text
|
|
style={{ color: theme.rejectButton }}
|
|
className="text-sm flex-1"
|
|
>
|
|
Konflikt: {conflict.title} ({formatTime(conflict.startTime)}{" "}
|
|
- {formatTime(conflict.endTime)})
|
|
</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
<ActionButtons
|
|
isDisabled={isDisabled}
|
|
respondedAction={proposedChange.respondedAction}
|
|
showEdit={proposedChange.action !== "delete" && !isDisabled}
|
|
onConfirm={() => onConfirm(proposedChange)}
|
|
onReject={onReject}
|
|
onEdit={onEdit ? () => onEdit(proposedChange) : undefined}
|
|
/>
|
|
</EventCardBase>
|
|
</View>
|
|
);
|
|
};
|