* fix(app/e2e): fix composer-lock test — use mock provider + prompt so lock releases
Two bugs in the "composer is locked while new workspace agent is being
created" test:
1. expectComposerDisabled used toBeDisabled() but React Native TextInput
with editable={false} renders as <textarea readonly> on web, not
<textarea disabled>. Fixed to not.toBeEditable().
2. clickNewWorkspaceButton created an agent with no prompt, leaving the
agent permanently "idle". showPendingCreateSubmitLoading only clears
when authoritativeStatus is non-bootstrapping, but "idle" counts as
bootstrapping — so the composer lock never released. Fixed by opening
the new-workspace composer, filling a prompt, then clicking Create so
the mock agent transitions to "running" after agent_created is released.
Also switches buildCreateAgentPreferences to provider "mock" with the
ten-second-stream model so E2E tests exercise app behavior without
depending on real provider availability.
* fix(app): add aria-checked to Switch for E2E toBeChecked() assertions
React Native Web does not map accessibilityState.checked to aria-checked
for role="switch", so Playwright's toBeChecked() always finds the element
unchecked. Adding aria-checked={value} directly to the Pressable sets the
attribute explicitly.
Update the switch.test.tsx mock to accept and pass through the explicit
aria-checked prop, keeping the mock faithful to the fixed component.
130 lines
3.0 KiB
TypeScript
130 lines
3.0 KiB
TypeScript
import { useCallback, useMemo } from "react";
|
|
import {
|
|
Pressable,
|
|
type GestureResponderEvent,
|
|
type StyleProp,
|
|
type ViewStyle,
|
|
} from "react-native";
|
|
import Animated, {
|
|
Easing,
|
|
interpolateColor,
|
|
useAnimatedStyle,
|
|
useDerivedValue,
|
|
withTiming,
|
|
} from "react-native-reanimated";
|
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
|
|
interface SwitchProps {
|
|
value: boolean;
|
|
onValueChange?: (value: boolean) => void;
|
|
disabled?: boolean;
|
|
accessibilityLabel?: string;
|
|
testID?: string;
|
|
style?: StyleProp<ViewStyle>;
|
|
}
|
|
|
|
const TRACK = { width: 34, height: 20 };
|
|
const THUMB = 16;
|
|
|
|
const TIMING = { duration: 180, easing: Easing.inOut(Easing.ease) };
|
|
|
|
export function Switch({
|
|
value,
|
|
onValueChange,
|
|
disabled = false,
|
|
accessibilityLabel,
|
|
testID,
|
|
style,
|
|
}: SwitchProps) {
|
|
const { theme } = useUnistyles();
|
|
const track = TRACK;
|
|
const thumb = THUMB;
|
|
const padding = (track.height - thumb) / 2;
|
|
const thumbTravel = track.width - thumb - padding * 2;
|
|
|
|
const progress = useDerivedValue(() => withTiming(value ? 1 : 0, TIMING));
|
|
|
|
const trackAnimatedStyle = useAnimatedStyle(() => ({
|
|
backgroundColor: interpolateColor(
|
|
progress.value,
|
|
[0, 1],
|
|
[theme.colors.surface3, theme.colors.accent],
|
|
),
|
|
}));
|
|
|
|
const thumbAnimatedStyle = useAnimatedStyle(() => ({
|
|
transform: [{ translateX: progress.value * thumbTravel }],
|
|
}));
|
|
|
|
const handlePress = useCallback(
|
|
(event: GestureResponderEvent) => {
|
|
event.stopPropagation();
|
|
if (disabled) return;
|
|
onValueChange?.(!value);
|
|
},
|
|
[disabled, onValueChange, value],
|
|
);
|
|
|
|
const accessibilityState = useMemo(() => ({ checked: value, disabled }), [value, disabled]);
|
|
const pressableStyle = useMemo(
|
|
() => [disabled ? styles.disabled : null, style],
|
|
[disabled, style],
|
|
);
|
|
const trackStyle = useMemo(
|
|
() => [
|
|
styles.track,
|
|
{
|
|
width: track.width,
|
|
height: track.height,
|
|
borderRadius: track.height / 2,
|
|
padding,
|
|
},
|
|
trackAnimatedStyle,
|
|
],
|
|
[track.width, track.height, padding, trackAnimatedStyle],
|
|
);
|
|
const thumbStyle = useMemo(
|
|
() => [
|
|
styles.thumb,
|
|
{ width: thumb, height: thumb, borderRadius: thumb / 2 },
|
|
thumbAnimatedStyle,
|
|
],
|
|
[thumb, thumbAnimatedStyle],
|
|
);
|
|
|
|
return (
|
|
<Pressable
|
|
onPress={handlePress}
|
|
disabled={disabled}
|
|
hitSlop={8}
|
|
accessibilityRole="switch"
|
|
accessibilityState={accessibilityState}
|
|
accessibilityLabel={accessibilityLabel}
|
|
aria-checked={value}
|
|
testID={testID}
|
|
style={pressableStyle}
|
|
>
|
|
<Animated.View style={trackStyle}>
|
|
<Animated.View style={thumbStyle} />
|
|
</Animated.View>
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
track: {
|
|
justifyContent: "center",
|
|
},
|
|
thumb: {
|
|
backgroundColor: theme.colors.palette.white,
|
|
shadowColor: "rgba(0, 0, 0, 0.25)",
|
|
shadowOffset: { width: 0, height: 1 },
|
|
shadowRadius: 2,
|
|
shadowOpacity: 1,
|
|
elevation: 2,
|
|
},
|
|
disabled: {
|
|
opacity: theme.opacity[50],
|
|
},
|
|
}));
|