This commit is contained in:
shafi54 2026-09-14 23:34:10 +05:30
parent d0b74fb61a
commit b5274e0c2b
34 changed files with 1598 additions and 11 deletions

View file

@ -81,7 +81,18 @@
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo && npx tsc --noEmit -p tests 2>&1 | tail -5; echo \"tsc_exit=$?\"; bun run test:e2e > /tmp/e2e_run9.log 2>&1; echo \"exit=$?\" >> /tmp/e2e_run9.log)", "Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo && npx tsc --noEmit -p tests 2>&1 | tail -5; echo \"tsc_exit=$?\"; bun run test:e2e > /tmp/e2e_run9.log 2>&1; echo \"exit=$?\" >> /tmp/e2e_run9.log)",
"Shell(DEBUG=pw:browser npx playwright test tests/specs/smoke.spec.ts 2 >& 1)", "Shell(DEBUG=pw:browser npx playwright test tests/specs/smoke.spec.ts 2 >& 1)",
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo && git diff product_to_sku_c1 -- apps/user-ui/hooks/cart-query-hooks.tsx apps/user-ui/hooks/useUploadToObjectStore.ts apps/user-ui/eas.json apps/user-ui/'(drawer)' 2>/dev/null; git diff product_to_sku_c1 -- \"apps/user-ui/app/(drawer)/(tabs)/me/addresses/index.tsx\" \"apps/user-ui/app/(drawer)/(tabs)/me/my-orders/index.tsx\" | head -160)", "Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo && git diff product_to_sku_c1 -- apps/user-ui/hooks/cart-query-hooks.tsx apps/user-ui/hooks/useUploadToObjectStore.ts apps/user-ui/eas.json apps/user-ui/'(drawer)' 2>/dev/null; git diff product_to_sku_c1 -- \"apps/user-ui/app/(drawer)/(tabs)/me/addresses/index.tsx\" \"apps/user-ui/app/(drawer)/(tabs)/me/my-orders/index.tsx\" | head -160)",
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo/packages/db_helper_sqlite && npx tsc --noEmit 2>&1 | tail -8; echo \"sqlite_exit=$?\"; echo \"=== user-ui ===\"; cd /Users/mohammedshafiuddin/WebDev/freshyo/apps/user-ui && npx tsc --noEmit 2>&1 | tee /tmp/uui_tsc.txt | tail -5; echo \"userui_errors=$(grep -cE 'error TS' /tmp/uui_tsc.txt)\")" "Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo/packages/db_helper_sqlite && npx tsc --noEmit 2>&1 | tail -8; echo \"sqlite_exit=$?\"; echo \"=== user-ui ===\"; cd /Users/mohammedshafiuddin/WebDev/freshyo/apps/user-ui && npx tsc --noEmit 2>&1 | tee /tmp/uui_tsc.txt | tail -5; echo \"userui_errors=$(grep -cE 'error TS' /tmp/uui_tsc.txt)\")",
"Shell(/.maestro/bin/maestro:*)",
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo && adb shell am force-stop in.freshyo.app; adb shell monkey -p in.freshyo.app -c android.intent.category.LAUNCHER 1 >/dev/null 2>&1; sleep 8; mkdir -p \"$COMMANDCODE_SCRATCHPAD\"; adb exec-out screencap -p > \"$COMMANDCODE_SCRATCHPAD/screen1.png\"; ls -la \"$COMMANDCODE_SCRATCHPAD/screen1.png\")",
"Shell(adb:*)",
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo && adb exec-out screencap -p > \"$COMMANDCODE_SCRATCHPAD/screen2.png\" && ls -la \"$COMMANDCODE_SCRATCHPAD/screen2.png\")",
"Shell(bash e2e/run.sh flows/smoke.yaml 2 >& 1)",
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo && \"$HOME/.maestro/bin/maestro\" hierarchy --platform android 2>/dev/null > \"$COMMANDCODE_SCRATCHPAD/hier.json\"; wc -c \"$COMMANDCODE_SCRATCHPAD/hier.json\"; echo \"=== occurrences ===\"; grep -o \"product-card\" \"$COMMANDCODE_SCRATCHPAD/hier.json\" | head -3; echo \"--- context around testID usages ---\"; grep -oE '\"(resource-id|content-desc|text|accessibilityText)\": \"[^\"]{0,40}\"' \"$COMMANDCODE_SCRATCHPAD/hier.json\" | sort | uniq -c | sort -rn | head -20)",
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo && adb shell uiautomator dump /sdcard/ui.xml >/dev/null 2>&1; adb shell cat /sdcard/ui.xml > \"$COMMANDCODE_SCRATCHPAD/ui.xml\" 2>/dev/null; wc -c \"$COMMANDCODE_SCRATCHPAD/ui.xml\"; echo \"=== product-card? ===\"; grep -o \"product-card\" \"$COMMANDCODE_SCRATCHPAD/ui.xml\" | head -2; echo \"=== sample attrs ===\"; grep -oE 'content-desc=\"[^\"]{1,40}\"' \"$COMMANDCODE_SCRATCHPAD/ui.xml\" | sort -u | head -15; echo \"=== text sample ===\"; grep -oE 'text=\"[^\"]{2,40}\"' \"$COMMANDCODE_SCRATCHPAD/ui.xml\" | sort -u | head -20)",
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo && adb shell am force-stop in.freshyo.app; adb shell monkey -p in.freshyo.app -c android.intent.category.LAUNCHER 1 >/dev/null 2>&1; sleep 16; adb shell uiautomator dump /sdcard/ui2.xml >/dev/null 2>&1; adb shell cat /sdcard/ui2.xml > \"$COMMANDCODE_SCRATCHPAD/ui2.xml\" 2>/dev/null; echo \"size: $(wc -c < \"$COMMANDCODE_SCRATCHPAD/ui2.xml\")\"; echo \"=== product-card occurrences ===\"; grep -c \"product-card\" \"$COMMANDCODE_SCRATCHPAD/ui2.xml\" || true; echo \"=== resource-id sample ===\"; grep -oE 'resource-id=\"[^\"]{1,50}\"' \"$COMMANDCODE_SCRATCHPAD/ui2.xml\" | sort -u | head -15; echo \"=== content-desc sample ===\"; grep -oE 'content-desc=\"[^\"]{2,40}\"' \"$COMMANDCODE_SCRATCHPAD/ui2.xml\" | sort -u | head -15)",
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo && cat > \"$COMMANDCODE_SCRATCHPAD/probe2.yaml\" <<'YAML' appId: in.freshyo.app --- - assertVisible: id: \"product-card\" YAML \"$HOME/.maestro/bin/maestro\" test \"$COMMANDCODE_SCRATCHPAD/probe2.yaml\" --platform android 2>&1 | tail -6)",
"Shell(bash e2e/run.sh flows/place-order.yaml 2 >& 1)",
"Shell(cd /Users/mohammedshafiuddin/WebDev/freshyo && ls ~/.maestro/tests/2026-09-14_154602/place-order/screen-hierarchy/ 2>/dev/null; F=$(ls ~/.maestro/tests/2026-09-14_154602/place-order/screen-hierarchy/*Me* 2>/dev/null | head -1); echo \"file: $F\"; grep -oE '\"(text|accessibilityText|resource-id)\": \"[^\"]{1,30}\"' \"$F\" 2>/dev/null | sort -u | head -40)"
], ],
"deny": [], "deny": [],
"defaultMode": "default" "defaultMode": "default"

View file

@ -124,6 +124,12 @@ er-ui/app/(drawer)/(tabs)/home/index.tsx, I want similar here") and expects the
- Prefers that disruptive user-facing dialogs be reserved for genuinely important events: the OTA update prompt should NOT be shown for every update — only for a few crucial releases ("I don't want to show this for all the updates. Only a few updates are so important"), while routine updates apply silently in the background. Confidence: 0.7 - Prefers that disruptive user-facing dialogs be reserved for genuinely important events: the OTA update prompt should NOT be shown for every update — only for a few crucial releases ("I don't want to show this for all the updates. Only a few updates are so important"), while routine updates apply silently in the background. Confidence: 0.7
- Uses "quantity" loosely to refer to the pack size / unit notation on order items: he reported "we aren't showing quantity" about a row that already rendered the numeric count, then clarified "yeah, show the pack size" — so when he says a quantity isn't shown, check whether he actually means the unit/pack-size notation is missing. Confidence: 0.55 - Uses "quantity" loosely to refer to the pack size / unit notation on order items: he reported "we aren't showing quantity" about a row that already rendered the numeric count, then clarified "yeah, show the pack size" — so when he says a quantity isn't shown, check whether he actually means the unit/pack-size notation is missing. Confidence: 0.55
- For new operational switches/flags, prefers a backend constant in the admin-editable constants store (CONST_KEYS + seed + exposed via essentialConsts, editable in Customize App) over build/publish-time config (e.g. an `expo.extra` flag in app.json) or hardcoding — chosen when offered the trade-off ("let's introduce a new constant"), because it can be changed remotely with no rebuild or republish. Confidence: 0.6 - For new operational switches/flags, prefers a backend constant in the admin-editable constants store (CONST_KEYS + seed + exposed via essentialConsts, editable in Customize App) over build/publish-time config (e.g. an `expo.extra` flag in app.json) or hardcoding — chosen when offered the trade-off ("let's introduce a new constant"), because it can be changed remotely with no rebuild or republish. Confidence: 0.6
- Dislikes long lists of CLI flags for supplying run-time/test values and asks for a "better way" ("isn't there a better way to supply the values"). Expects values supplied declaratively: non-secret defaults committed in a config file (e.g. Maestro `config.yaml` `env:` block), secrets in a gitignored `.env`-style file, and a thin wrapper/runner script wired to a single package-manager entry point (`bun run e2e`) so one short command runs everything — with extra flags still passable through. Secrets should never be passed inline on the command line or committed. Confidence: 0.7
- When he doubts the current approach ("isn't there a better way"), expects the agent to actually research the tool's documented capabilities (check `--help`, official docs) before proposing alternatives, rather than answering from memory. Confidence: 0.6
- For mobile app E2E testing, wants Android ONLY and never iOS — stated explicitly: "I want to test only on android. Never on ios." The test runner/harness should be wired so it can never target an iOS simulator (e.g., pin the Android package id and always pass `--platform android`), rather than auto-detecting the platform from booted devices. Confidence: 0.9
- For React Native / Expo mobile E2E automation, prefers **Maestro** (not Detox/Appium), with its flow configs in a folder at the repo ROOT named `e2e/` — "I want test end to end test automation with maestro. have a folder at the root named e2e" — mirroring his `tests/`-at-root convention for Playwright. Expects the tool's own YAML workspace layout inside it (`config.yaml`, `flows/`, reusable `subflows/`), not per-app test dirs. Confidence: 0.85
- Wants E2E flows to take the shortest realistic user path and skip unnecessary intermediate screens — e.g., add to the cart directly from the home product card's cart icon instead of opening the product-detail screen first ("don't open any product detail. Just click on the cart icon of any product, then go to the cart page and then place order"). Confidence: 0.5
- When he pastes a runtime error/log (e.g., a tRPC/D1 `Failed query: select ... from "key_val_store"` 500), he asks for an explanation first — "what does this error mean. Why am I seeing it" — expecting the agent to investigate and explain the root cause (local D1 state, migrations/config) before changing any code, not to jump straight to a fix. Confidence: 0.5
e changes. Confidence: 0.95 e changes. Confidence: 0.95
er-ui/app/(drawer)/(tabs)/home/index.tsx, I want similar here") and expects the agent to mirror that pattern exactly, including details like item counts and container styling. Confidence: 0.9 er-ui/app/(drawer)/(tabs)/home/index.tsx, I want similar here") and expects the agent to mirror that pattern exactly, including details like item counts and container styling. Confidence: 0.9
als) rather than introducing new brand palettes; approval of a redesign's layout/composition does not imply approval of color or theme changes. Confidence: 0.95 als) rather than introducing new brand palettes; approval of a redesign's layout/composition does not imply approval of color or theme changes. Confidence: 0.95

10
.gitignore vendored
View file

@ -132,4 +132,12 @@ dist
.pnp.* .pnp.*
type-clusters/* type-clusters/*
# ---> Maestro (root E2E suite)
/e2e/.maestro-output/
/e2e/.env
apps/user-ui/android/
apps/admin-ui/android/
.maestro-output/*

View file

@ -198,6 +198,7 @@ export default function SlotDetails() {
{/* FAB for Edit Slot */} {/* FAB for Edit Slot */}
<MyTouchableOpacity <MyTouchableOpacity
testID="edit-slot-fab"
onPress={() => router.push(`/(drawer)/dashboard/slots/edit/${slot.id}` as any)} onPress={() => router.push(`/(drawer)/dashboard/slots/edit/${slot.id}` as any)}
activeOpacity={0.95} activeOpacity={0.95}
style={{ position: 'absolute', bottom: 32, right: 24, zIndex: 100 }} style={{ position: 'absolute', bottom: 32, right: 24, zIndex: 100 }}

View file

@ -28,6 +28,7 @@ interface ProductsSelectorProps {
selectedGroupIds?: number[]; selectedGroupIds?: number[];
onGroupChange?: (groupIds: number[]) => void; onGroupChange?: (groupIds: number[]) => void;
showGroups?: boolean; showGroups?: boolean;
testID?: string;
} }
export default function ProductsSelector({ export default function ProductsSelector({
@ -44,6 +45,7 @@ export default function ProductsSelector({
showGroups = true, showGroups = true,
selectedGroupIds = [], selectedGroupIds = [],
onGroupChange, onGroupChange,
testID,
}: ProductsSelectorProps) { }: ProductsSelectorProps) {
const { data: skusData } = trpc.common.product.getAllSkusSummary.useQuery(); const { data: skusData } = trpc.common.product.getAllSkusSummary.useQuery();
const allSkus: SkuSummary[] = skusData?.skus || []; const allSkus: SkuSummary[] = skusData?.skus || [];
@ -147,6 +149,7 @@ export default function ProductsSelector({
)} )}
<BottomDropdown <BottomDropdown
testID={testID}
label={label} label={label}
options={productOptions} options={productOptions}
value={multiple value={multiple

View file

@ -160,6 +160,7 @@ export default function SlotForm({
selectedGroupIds={values.selectedGroupIds} selectedGroupIds={values.selectedGroupIds}
onGroupChange={(newGroupIds) => setFieldValue('selectedGroupIds', newGroupIds)} onGroupChange={(newGroupIds) => setFieldValue('selectedGroupIds', newGroupIds)}
label="Select Products" label="Select Products"
testID="slot-products-selector"
placeholder="Select products for this slot" placeholder="Select products for this slot"
/> />
</View> </View>
@ -193,6 +194,7 @@ export default function SlotForm({
selectedGroupIds={snippet.groupIds || []} selectedGroupIds={snippet.groupIds || []}
onGroupChange={(newGroupIds) => setFieldValue(`vendorSnippetList.${index}.groupIds`, newGroupIds)} onGroupChange={(newGroupIds) => setFieldValue(`vendorSnippetList.${index}.groupIds`, newGroupIds)}
label="Select Products" label="Select Products"
testID="snippet-products-selector"
placeholder="Select products for snippet" placeholder="Select products for snippet"
isDisabled={(sku) => !values.selectedSkuIds.includes(sku.id)} isDisabled={(sku) => !values.selectedSkuIds.includes(sku.id)}
/> />

View file

@ -8,10 +8,10 @@ routes = [
[[d1_databases]] [[d1_databases]]
binding = "DB" binding = "DB"
database_name = "freshyo-backend-dev" #database_name = "freshyo-backend-dev"
database_id = "b2528bdb-0ec0-478e-8765-b118918153c0" #database_id = "b2528bdb-0ec0-478e-8765-b118918153c0"
#database_name = "freshyo-dev" database_name = "freshyo-dev"
#database_id = "3cad440f-dc14-4baa-ab05-04eb08e206de" database_id = "3cad440f-dc14-4baa-ab05-04eb08e206de"
migrations_dir="../../packages/db_helper_sqlite/drizzle" migrations_dir="../../packages/db_helper_sqlite/drizzle"
migrations_pattern="migration.sql" migrations_pattern="migration.sql"
[durable_objects] [durable_objects]

View file

@ -16,6 +16,7 @@ function Login() {
const { loginWithToken } = useAuth(); const { loginWithToken } = useAuth();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [step, setStep] = useState<'mobile' | 'choice' | 'otp' | 'password'>('mobile'); const [step, setStep] = useState<'mobile' | 'choice' | 'otp' | 'password'>('mobile');
const [passwordBackTo, setPasswordBackTo] = useState<'mobile' | 'choice' | 'otp'>('choice');
const [selectedMobile, setSelectedMobile] = useState(''); const [selectedMobile, setSelectedMobile] = useState('');
const [canResend, setCanResend] = useState(false); const [canResend, setCanResend] = useState(false);
const [resendCountdown, setResendCountdown] = useState(0); const [resendCountdown, setResendCountdown] = useState(0);
@ -135,8 +136,9 @@ function Login() {
setError, setError,
clearErrors, clearErrors,
setValue, setValue,
watch,
} = useForm<LoginFormInputs>({ } = useForm<LoginFormInputs>({
defaultValues: { mobile: "", otp: "", password: "" }, defaultValues: { mobile: "", otp: "", identifier: "", password: "" },
}); });
const validateMobile = (mobile: string): boolean => { const validateMobile = (mobile: string): boolean => {
@ -223,7 +225,17 @@ function Login() {
otp: data.otp, otp: data.otp,
}); });
} else if (step === 'password') { } else if (step === 'password') {
// Login with password // Login with username/email/mobile + password
const identifier = (data.identifier || '').trim() || selectedMobile;
if (!identifier) {
setError("identifier", {
type: "manual",
message: "Username is required",
});
return;
}
if (!data.password || data.password.length < 6) { if (!data.password || data.password.length < 6) {
setError("password", { setError("password", {
type: "manual", type: "manual",
@ -234,7 +246,7 @@ function Login() {
try { try {
const response = await loginMutation.mutateAsync({ const response = await loginMutation.mutateAsync({
identifier: selectedMobile, identifier,
password: data.password, password: data.password,
}); });
loginWithToken(response.data.token, response.data.user); loginWithToken(response.data.token, response.data.user);
@ -313,7 +325,11 @@ function Login() {
<View style={tw`flex-row justify-between mb-6`}> <View style={tw`flex-row justify-between mb-6`}>
<MyButton <MyButton
textContent="Use Password" textContent="Use Password"
onPress={() => setStep('password')} onPress={() => {
setPasswordBackTo('choice');
setValue('identifier', selectedMobile);
setStep('password');
}}
fillColor="gray1" fillColor="gray1"
textColor="black1" textColor="black1"
style={tw`flex-1 mr-2 border border-gray-200`} style={tw`flex-1 mr-2 border border-gray-200`}
@ -374,6 +390,8 @@ function Login() {
<MyTouchableOpacity <MyTouchableOpacity
onPress={() => { onPress={() => {
setPasswordBackTo('choice');
setValue('identifier', selectedMobile);
setStep('password'); setStep('password');
setValue('otp', ''); setValue('otp', '');
setOtpCells(['', '', '', '']); setOtpCells(['', '', '', '']);
@ -422,6 +440,32 @@ function Login() {
{step === 'password' && ( {step === 'password' && (
<> <>
<Controller
control={control}
name="identifier"
render={({ field: { onChange, onBlur, value } }) => (
<View style={tw`mb-4`}>
<MyTextInput
topLabel="Username"
placeholder="Enter your email or mobile"
value={value}
onChangeText={onChange}
onBlur={onBlur}
autoCapitalize="none"
autoCorrect={false}
keyboardType="email-address"
style={tw`bg-gray-50`}
error={!!errors.identifier}
/>
</View>
)}
/>
{errors.identifier && (
<MyText style={tw`text-red-500 text-sm mb-4 text-center`}>
{errors.identifier.message}
</MyText>
)}
<Controller <Controller
control={control} control={control}
name="password" name="password"
@ -450,7 +494,7 @@ function Login() {
<View style={tw`flex-row justify-center mb-6`}> <View style={tw`flex-row justify-center mb-6`}>
<MyTouchableOpacity <MyTouchableOpacity
onPress={() => { onPress={() => {
setStep('choice'); setStep(passwordBackTo);
setValue('password', ''); setValue('password', '');
clearErrors(); clearErrors();
}} }}
@ -502,6 +546,22 @@ function Login() {
: (step === 'otp' ? "Verify & Login" : step === 'password' ? "Login" : "Continue")} : (step === 'otp' ? "Verify & Login" : step === 'password' ? "Login" : "Continue")}
</MyButton> </MyButton>
</View> </View>
{step === 'mobile' && (
<MyTouchableOpacity
onPress={() => {
setPasswordBackTo('mobile');
setValue('identifier', watch('mobile') || '');
setStep('password');
clearErrors();
}}
style={tw`mt-5`}
>
<MyText weight="semibold" style={tw`text-brand600 text-center`}>
other ways to login
</MyText>
</MyTouchableOpacity>
)}
</> </>
)} )}
</View> </View>

View file

@ -403,6 +403,7 @@ export default function OrderDetails() {
<MyText style={tw`text-gray-700 font-medium mb-2`}>Reason for cancellation</MyText> <MyText style={tw`text-gray-700 font-medium mb-2`}>Reason for cancellation</MyText>
<TextInput <TextInput
testID="cancel-reason"
style={tw`bg-gray-50 border border-gray-200 rounded-xl p-4 min-h-24 text-base text-gray-800 mb-6`} style={tw`bg-gray-50 border border-gray-200 rounded-xl p-4 min-h-24 text-base text-gray-800 mb-6`}
value={cancelReason} value={cancelReason}
onChangeText={setCancelReason} onChangeText={setCancelReason}

View file

@ -337,6 +337,7 @@ const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
</View> </View>
)} )}
<MyTouchableOpacity <MyTouchableOpacity
testID="place-order-button"
style={tw`bg-brand500 py-4 rounded-xl items-center shadow-md ${!selectedAddress ? 'opacity-50' : ''}`} style={tw`bg-brand500 py-4 rounded-xl items-center shadow-md ${!selectedAddress ? 'opacity-50' : ''}`}
disabled={!selectedAddress} disabled={!selectedAddress}
onPress={handlePlaceOrder} onPress={handlePlaceOrder}

View file

@ -156,6 +156,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
return ( return (
<ContainerComp> <ContainerComp>
<MyTouchableOpacity <MyTouchableOpacity
testID="product-card"
style={[ style={[
tw`bg-white overflow-hidden border border-gray-200`, tw`bg-white overflow-hidden border border-gray-200`,
isHero ? tw`rounded-xl pb-1.5` : tw`rounded-2xl pb-2`, isHero ? tw`rounded-xl pb-1.5` : tw`rounded-2xl pb-2`,
@ -205,6 +206,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
<MiniQuantifier value={quantity} onChange={handleQuantityChange} step={item.incrementStep} /> <MiniQuantifier value={quantity} onChange={handleQuantityChange} step={item.incrementStep} />
) : ( ) : (
<MyTouchableOpacity <MyTouchableOpacity
testID="add-to-cart-icon"
style={isHero ? tw`w-7 h-7 rounded-full bg-white items-center justify-center shadow-md` : tw`w-8 h-8 rounded-full bg-white items-center justify-center shadow-md`} style={isHero ? tw`w-7 h-7 rounded-full bg-white items-center justify-center shadow-md` : tw`w-8 h-8 rounded-full bg-white items-center justify-center shadow-md`}
onPress={() => handleQuantityChange(1)} onPress={() => handleQuantityChange(1)}
activeOpacity={0.8} activeOpacity={0.8}

View file

@ -958,6 +958,7 @@ export default function CartPage({ isFlashDelivery = false }: FlashDeliveryProps
<MyText style={tw`text-gray-700 font-bold`}>Shop More</MyText> <MyText style={tw`text-gray-700 font-bold`}>Shop More</MyText>
</MyTouchableOpacity> </MyTouchableOpacity>
<MyTouchableOpacity <MyTouchableOpacity
testID="checkout-button"
style={tw`flex-1 bg-brand500 py-3.5 rounded-xl items-center shadow-md flex-row justify-center`} style={tw`flex-1 bg-brand500 py-3.5 rounded-xl items-center shadow-md flex-row justify-center`}
onPress={() => { onPress={() => {
const availableItems = cartItems const availableItems = cartItems

View file

@ -211,6 +211,7 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
</View> </View>
<MyTouchableOpacity <MyTouchableOpacity
testID="go-to-cart"
style={tw`bg-white px-3 py-2 rounded-2xl shadow-lg shadow-black/20`} style={tw`bg-white px-3 py-2 rounded-2xl shadow-lg shadow-black/20`}
onPress={() => router.push( onPress={() => router.push(
isFlashDelivery isFlashDelivery

View file

@ -162,6 +162,7 @@ export default function AddToCartDialog() {
{availableSlots.map((slot: any) => ( {availableSlots.map((slot: any) => (
<MyTouchableOpacity <MyTouchableOpacity
key={slot.id} key={slot.id}
testID="slot-option"
style={tw`flex-row items-start mb-4 bg-gray-50 p-4 rounded-xl border border-gray-100 ${selectedSlotId === slot.id ? 'border-brand500' : 'border-gray-100' style={tw`flex-row items-start mb-4 bg-gray-50 p-4 rounded-xl border border-gray-100 ${selectedSlotId === slot.id ? 'border-brand500' : 'border-gray-100'
}`} }`}
onPress={() => { onPress={() => {
@ -241,6 +242,7 @@ export default function AddToCartDialog() {
<View style={tw`flex-row gap-3 mt-4 pb-12`}> <View style={tw`flex-row gap-3 mt-4 pb-12`}>
<MyTouchableOpacity <MyTouchableOpacity
testID="add-to-cart-confirm"
style={tw`flex-1 bg-brand500 py-3.5 rounded-xl items-center ${(!selectedSlotId && !selectedFlashDelivery) ? 'opacity-50' : ''}`} style={tw`flex-1 bg-brand500 py-3.5 rounded-xl items-center ${(!selectedSlotId && !selectedFlashDelivery) ? 'opacity-50' : ''}`}
onPress={handleAddToCart} onPress={handleAddToCart}
disabled={(addToCart.isLoading || updateCartItem.isLoading) || (!selectedSlotId && !selectedFlashDelivery)} disabled={(addToCart.isLoading || updateCartItem.isLoading) || (!selectedSlotId && !selectedFlashDelivery)}

View file

@ -113,6 +113,7 @@ const AddressForm: React.FC<AddressFormProps> = ({ onSuccess, initialValues, isE
{({ handleChange, handleBlur, handleSubmit, values, errors, touched, setFieldValue }) => ( {({ handleChange, handleBlur, handleSubmit, values, errors, touched, setFieldValue }) => (
<View style={tw`flex-col gap-2`}> <View style={tw`flex-col gap-2`}>
<MyTextInput <MyTextInput
testID="address-name"
placeholder="Name" placeholder="Name"
shrunkPadding={true} shrunkPadding={true}
onChangeText={handleChange('name')} onChangeText={handleChange('name')}
@ -122,6 +123,7 @@ const AddressForm: React.FC<AddressFormProps> = ({ onSuccess, initialValues, isE
{touched.name && errors.name && <MyText style={tw`text-red-500 mb-2`}>{errors.name}</MyText>} {touched.name && errors.name && <MyText style={tw`text-red-500 mb-2`}>{errors.name}</MyText>}
<MyTextInput <MyTextInput
testID="address-phone"
placeholder="Phone" placeholder="Phone"
shrunkPadding={true} shrunkPadding={true}
keyboardType="phone-pad" keyboardType="phone-pad"
@ -132,6 +134,7 @@ const AddressForm: React.FC<AddressFormProps> = ({ onSuccess, initialValues, isE
{touched.phone && errors.phone && <MyText style={tw`text-red-500 mb-2`}>{errors.phone}</MyText>} {touched.phone && errors.phone && <MyText style={tw`text-red-500 mb-2`}>{errors.phone}</MyText>}
<MyTextInput <MyTextInput
testID="address-line1"
placeholder="Address Line 1" placeholder="Address Line 1"
shrunkPadding={true} shrunkPadding={true}
onChangeText={handleChange('addressLine1')} onChangeText={handleChange('addressLine1')}
@ -227,6 +230,7 @@ const AddressForm: React.FC<AddressFormProps> = ({ onSuccess, initialValues, isE
</View> </View>
<MyTouchableOpacity <MyTouchableOpacity
testID="address-submit"
style={tw`bg-indigo-600 p-3 rounded ${isSubmitting ? 'opacity-50' : ''}`} style={tw`bg-indigo-600 p-3 rounded ${isSubmitting ? 'opacity-50' : ''}`}
onPress={() => handleSubmit()} onPress={() => handleSubmit()}
disabled={isSubmitting} disabled={isSubmitting}

View file

@ -1660,3 +1660,373 @@ unhandled by the consumer — unchanged by this edit.
NOTE: db_helper_postgres order helpers left untouched (dormant package, pre-existing drift). NOTE: db_helper_postgres order helpers left untouched (dormant package, pre-existing drift).
Order list screens (my-orders/index.tsx, web-ui me.orders.tsx) unchanged — they render item rows too Order list screens (my-orders/index.tsx, web-ui me.orders.tsx) unchanged — they render item rows too
but were not in scope for this request. but were not in scope for this request.
[2026-09-14 11:41:28] user-ui login: add "other ways to login" link -> username + password fields.
=== packages/shared/types/app-common.types.ts ===
- LoginFormInputs: + identifier?: string
=== apps/user-ui/app/(auth)/login.tsx ===
- new state: const [passwordBackTo, setPasswordBackTo] = useState<'mobile' | 'choice' | 'otp'>('choice')
- useForm: destructure `watch`; defaultValues + identifier: ""
- onSubmit (step === 'password'):
+ require data.identifier (setError 'identifier' when blank)
before: loginMutation.mutateAsync({ identifier: selectedMobile, password: data.password })
after: loginMutation.mutateAsync({ identifier: data.identifier.trim() || selectedMobile, password: data.password })
- password step: new Controller for `identifier` ABOVE the password field
topLabel "Username", placeholder "Enter your email or mobile",
autoCapitalize="none", keyboardType="email-address", error={!!errors.identifier}
+ error text for errors.identifier
- password step "Back to options" link: setStep('choice') -> setStep(passwordBackTo)
- choice step "Use Password" button: + setPasswordBackTo('choice'); setValue('identifier', selectedMobile)
- otp step "Or login with Password" link: + setPasswordBackTo('choice'); setValue('identifier', selectedMobile)
- NEW link below the primary button, rendered only on the mobile step:
<MyTouchableOpacity onPress={() => { setPasswordBackTo('mobile'); setValue('identifier', watch('mobile') || ''); setStep('password'); clearErrors() }} style={tw`mt-5`}>
<MyText weight="semibold" style={tw`text-brand600 text-center`}>other ways to login</MyText>
</MyTouchableOpacity>
[2026-09-14 12:23:51] ADD Maestro E2E suite for apps/user-ui (root e2e/ folder) + additive testIDs.
NEW FILES:
- e2e/config.yaml — Maestro workspace config (flows: flows/**/*.yaml; executionOrder smoke, place-order; env.APP_ID; testOutputDir .maestro-output).
- e2e/flows/smoke.yaml — launch app, assert home renders.
- e2e/flows/place-order.yaml — home -> product -> add to cart (slot) -> cart -> checkout (address if none) -> Place Order -> "Order Placed Successfully!".
- e2e/subflows/login.yaml — reusable: logs in via "other ways to login" (username+password) only when the login screen is visible.
- e2e/README.md — how to run, env vars, preconditions.
- e2e/.env.example — template for -e vars.
- .gitignore: + /e2e/.maestro-output/
ADDITIVE testIDs (no behaviour change; user-ui + common-ui primitives already forward testID):
- apps/user-ui/components/ProductCard.tsx: card root -> testID="product-card"
- apps/user-ui/src/components/AddToCartDialog.tsx: slot row -> testID="slot-option"; confirm button -> testID="add-to-cart-confirm"
- apps/user-ui/components/FloatingCartBar.tsx: "Go to Cart" -> testID="go-to-cart"
- apps/user-ui/components/cart-page.tsx: Checkout -> testID="checkout-button"
- apps/user-ui/components/PaymentAndOrderComponent.tsx: Place Order -> testID="place-order-button"
- apps/user-ui/src/components/AddressForm.tsx: Name/Phone/Address Line 1 inputs -> testID="address-name"/"address-phone"/"address-line1"; submit -> testID="address-submit"
[2026-09-14 12:41:00] CORRECTION to the above Maestro entry (found by running it with Maestro 2.7.0):
- e2e/config.yaml: `flows: ['flows/**/*.yaml']` matched NOTHING ("Flow inclusion pattern(s) did not match any Flow files") — Maestro globs use `dir/**`, not `**/*.yaml`. Changed to `flows: ['flows/**']`.
- Removed the `env: APP_ID` block: Maestro does NOT substitute workspace/env vars into a flow's `appId` header (it produced "Package undefined is not installed"). The `appId` is now hardcoded per flow (`com.freshyotrial.app`), with an Android note to swap in `in.freshyo.app`.
- e2e/README.md + e2e/.env.example updated to match (Android = edit the appId line, not `-e APP_ID`).
VERIFIED: `maestro test e2e` now discovers and runs the flows; it reaches `launchApp` and reports "Package com.freshyotrial.app is not installed" (expected here — no simulator/app installed). `npx tsc --noEmit` in apps/user-ui = 0 after the testID additions.
[2026-09-14 15:03:52] Maestro: supply flow values without long CLI flag lists.
- e2e/config.yaml: + env block with non-secret defaults (ADDRESS_NAME, ADDRESS_PHONE, ADDRESS_LINE1)
— Maestro resolves ${VAR} from config.yaml `env` in commands (still not in the `appId` header).
- NEW e2e/run.sh: `set -a; . e2e/.env; set +a; exec maestro test e2e "$@"` — loads the gitignored
e2e/.env automatically, so credentials live in one gitignored file instead of -e flags.
- package.json (root): + script "e2e": "bash e2e/run.sh".
- e2e/README.md + e2e/.env.example: document the three ways to supply values (config env defaults,
gitignored e2e/.env via `bun run e2e`, or `MAESTRO_`-prefixed shell vars / -e for CI).
[2026-09-14 15:19:35] Maestro: parameterise appId so Android needs no file edits.
Empirically verified (Maestro 2.7.0, probe flow with `appId: ${APP_ID}`):
- `-e APP_ID=...` (CLI) -> substituted ✓ ("Package xx.test.pkg is not installed")
- plain `APP_ID=...` -> NOT substituted ("Package undefined")
- `MAESTRO_APP_ID=...` -> NOT substituted ("Package undefined")
- config.yaml `env:` -> NOT substituted for appId (established earlier)
=> the only way to parameterise `appId` is the CLI `-e` flag.
- e2e/flows/smoke.yaml, e2e/flows/place-order.yaml, e2e/subflows/login.yaml:
`appId: com.freshyotrial.app` -> `appId: ${APP_ID}`
- e2e/run.sh: resolves the app id and passes `-e APP_ID=<id>`:
1. `APP_ID` from e2e/.env (or the shell) wins, else
2. auto-detect — a booted iOS simulator -> com.freshyotrial.app,
otherwise a connected Android device -> in.freshyo.app,
otherwise iOS default.
So `bash e2e/run.sh` works on both platforms without editing files.
- e2e/.env.example + e2e/README.md: document the optional APP_ID override and the
new no-edit Android workflow.
[2026-09-14 15:25:33] Maestro: Android-only (never target iOS).
- e2e/flows/smoke.yaml, e2e/flows/place-order.yaml, e2e/subflows/login.yaml:
appId: ${APP_ID} -> appId: in.freshyo.app (Android package, hardcoded).
- e2e/run.sh: removed the APP_ID resolution/auto-detection (iOS simulator sniffing)
and the `-e APP_ID=...` plumbing; now always passes `--platform android` so
Maestro can never pick a booted iOS simulator. Kept the .env loading,
MAESTRO_BIN fallback, and e2e-relative flow path handling.
- e2e/README.md: rewritten for Android-only (removed all iOS instructions).
- e2e/.env.example + e2e/config.yaml: comments updated to match.
[2026-09-14 15:45:00] Maestro Android run fixes (verified on emulator-5554, Pixel_3a_API_34).
- e2e/flows/smoke.yaml + e2e/subflows/login.yaml: replaced the "home is ready"
check. `assertVisible` takes no `timeout` (schema error: "Unknown Property:
timeout") -> use `extendedWaitUntil: {visible: {id: product-card}, timeout: 60000}`.
Also dropped the `scrollUntilVisible "All Available Products"` anchor: that
heading is far below the fold; `id: product-card` is visible immediately.
- e2e/flows/place-order.yaml: removed the scrollUntilVisible; tap
`id: product-card, index: 0` directly (cards are visible on home).
- e2e/run.sh: when an explicit flow path is passed, target ONLY that flow
(previously the workspace dir was passed too, so extra flows also ran).
VERIFIED: `bash e2e/run.sh flows/smoke.yaml` -> [Passed] smoke (12s).
Diagnostics: RN `testID` surfaces as Android `resource-id` (confirmed via
`uiautomator dump`: resource-id="product-card"/"go-to-cart"/"app-root"), and a
probe flow asserting `id: product-card` passes on the live app — the earlier
failures were cold-start timing, not selector mismatch.
STILL OPEN: `place-order` gets home -> product detail -> taps "Add to Cart" but
"Select Delivery Slot" is not found (38s); the detail screen stays on
"Add to Cart" with an empty cart. Needs one more on-device iteration on the
add-to-cart dialog.
NOTE: e2e/.env.example currently contains real-looking credentials in the working
tree; that file is committed — secrets belong in the gitignored e2e/.env.
[2026-09-14 15:40:02] Maestro place-order: add via the home card's cart icon (no product detail).
- apps/user-ui/components/ProductCard.tsx: the miniView add button (icon-only
CartIcon, previously no testID) -> testID="add-to-cart-icon".
- e2e/flows/place-order.yaml: replaced the "tap product-card -> product detail ->
Add to Cart" path with the home-only path:
tapOn {id: add-to-cart-icon, index: 0} # opens the Add-to-Cart dialog
-> tapOn {id: slot-option, index: 0}
-> tapOn {id: add-to-cart-confirm}
-> tapOn {id: go-to-cart} -> cart -> checkout -> place order.
No product-detail route is visited.
[2026-09-14 16:05:00] Maestro place-order: run fixes from on-device iteration (Pixel_3a_API_34).
- e2e/flows/place-order.yaml:
+ scrollUntilVisible {id: checkout-button} before tapping it (it sits below the
bill details, off-screen).
+ launchApp stays clearState:false and the login subflow stays conditional:
clearing state triggers the FirstUserWrapper onboarding ("Hello Happy
Shopping" / "Next"), and the bottom-tab labels aren't text-matchable, so the
suite can't walk to the login screen reliably. Documented as a precondition:
sign in once by hand on the emulator.
- e2e/subflows/login.yaml: back to the conditional `when: visible "other ways to
login"` form; ends by waiting for the bottom-nav "Home".
- e2e/flows/smoke.yaml: clearState:false, no login step (home is browsable as guest).
- e2e/run.sh: `shift`-based arg handling (bash 3.2 aborted on empty-array slicing
under `set -u`).
VERIFIED on the emulator: launch -> home card cart icon -> "Select Delivery Slot"
dialog -> slot picked -> Add to Cart -> "Go to Cart" -> cart page shows the item with
its slot -> scroll -> tap Checkout. Tapping Checkout while signed OUT redirects to
the login screen (useAuthenticatedRoute) — hence the pre-existing-session
precondition. A full run to a real "Order Placed Successfully!" was NOT executed
(it would create a real order).
[2026-09-14 16:30:00] Maestro place-order: PASSING end-to-end on emulator-5554.
Root causes found while iterating:
- e2e/run.sh sourced e2e/.env but never passed it on: Maestro does not read the
shell env, so ${TEST_IDENTIFIER}/${TEST_PASSWORD} resolved to "undefined" and
login failed with "Invalid credentials". Values are now forwarded as -e flags.
- The dev-build "Open debugger to view warnings" toast (bounds [28,1954][1053,2085])
overlays the bottom nav and swallowed taps on the Me tab; the flow no longer
touches the tab bar. (Me was also regex-matching the "Meat" tag chip.)
- clearState:true triggers the FirstUserWrapper onboarding ("Hello Happy Shopping"
/ "Next") — launchApp stays clearState:false.
- Sections below the fold (checkout-button, "Cash on Delivery",
place-order-button) need scrollUntilVisible; place-order-button uses
centerElement:true so the toast can't intercept the tap.
Flow changes (e2e/flows/place-order.yaml):
- Home card cart icon (id: add-to-cart-icon) -> "Select Delivery Slot" dialog ->
slot-option -> add-to-cart-confirm -> go-to-cart -> cart -> scroll -> Checkout.
- Sign-in is triggered by the checkout redirect (guest -> useAuthenticatedRoute ->
login screen), with waitForAnimationToEnd so the conditional login isn't skipped;
after login (which lands on Home) the flow re-enters the cart and checks out.
- Address is only added when "No addresses found" shows; COD is preselected.
Verified run: launch -> cart icon -> slot -> add -> cart -> checkout -> sign in ->
checkout -> place order -> "Order Placed Successfully!" / "Continue Shopping".
smoke.yaml also passes.
NOTE: that verification created a real order on the test account.
- e2e/.env.example: real-looking TEST_IDENTIFIER/TEST_PASSWORD blanked (it is
committed); the real values stay in the gitignored e2e/.env.
- e2e/README.md: documents the checkout-triggered login, the toast caveat, and the
scroll/centering requirements.
[2026-09-14 16:49:08] Maestro: after placing an order, check the home glimpse and cancel it.
apps/user-ui/app/(drawer)/(tabs)/me/my-orders/[id].tsx — add a testID to the
cancel-reason TextInput so the flow can target it instead of the placeholder:
<TextInput
+ testID="cancel-reason"
style={tw`bg-gray-50 border border-gray-200 rounded-xl p-4 min-h-24 text-base text-gray-800 mb-6`}
value={cancelReason}
e2e/flows/place-order.yaml — after the "Continue Shopping" confirmation, continue
into the home glimpse and cancel the order that was just placed:
+- # --- Home: the new order appears in the Next Order glimpse ------------------
+- tapOn: "Continue Shopping" # order-success -> dismissTo home
+- extendedWaitUntil:
+- visible: "Upcoming Order" # NextOrderGlimpse card header
+- timeout: 60000
+- assertVisible: "Track Order"
+- takeScreenshot: "e2e/.maestro-output/home-upcoming-order-glimpse"
+-
+- # --- Open it from the glimpse and cancel -----------------------------------
+- # The card sets the navigation target and replaces to /me, which forwards to
+- # /(drawer)/(tabs)/me/my-orders/{id}.
+- tapOn: "Upcoming Order"
+- extendedWaitUntil:
+- visible: "Order Items"
+- timeout: 30000
+- scrollUntilVisible: # Cancel Order sits below the summary
+- element:
+- text: "Cancel Order"
+- direction: DOWN
+- centerElement: true
+- tapOn: "Cancel Order"
+- extendedWaitUntil:
+- visible: "Reason for cancellation" # BottomDialog is open
+- timeout: 20000
+- tapOn:
+- id: "cancel-reason"
+- inputText: "Ordered by mistake"
+- hideKeyboard
+- tapOn: "Confirm Cancellation"
+- # Native success Alert, then the detail page refetches and shows the reason.
+- extendedWaitUntil:
+- visible: "Order cancelled successfully"
+- timeout: 20000
+- tapOn: "OK"
+- extendedWaitUntil:
+- visible: "Cancellation Reason"
+- timeout: 30000
Refinements found while getting it green (same files):
- The confirmation is preceded by a "Placing your order..." dialog, so
assertVisible on "Order Placed Successfully!" times out -> extendedWaitUntil 60s.
- NextOrderGlimpse renders BELOW "Our Stores" in the home header, so
extendedWaitUntil on "Upcoming Order" fails -> scrollUntilVisible
(centerElement: true, to keep clear of the dev toast).
- takeScreenshot takes a path relative to the test output dir, so the earlier
"e2e/.maestro-output/..." value nested a junk dir there -> plain name
"home-upcoming-order-glimpse"; it lands in
~/.maestro/tests/<ts>/place-order/takeScreenshot/.
- hideKeyboard then tapping "Confirm Cancellation" missed: the keyboard-dismiss
reflow moved the button. Added waitForAnimationToEnd + one conditional retry.
- The card's cart icon sits inside the card touchable, so the tap can fall
through and open Product Details -> conditional runFlow adds from there.
- launchApp resumes on whatever screen the previous run left open, which breaks
the first wait -> tap the Home tab with optional: true first.
- e2e/README.md: new "What place-order covers" section + notes on the above.
VERIFIED: full place-order run passes end to end — order placed, home glimpse
captured (#ORD2095, 1 item, CONFIRMED), order cancelled via Cancel Order ->
reason -> Confirm Cancellation -> "Order cancelled successfully" ->
"Cancellation Reason".
NOTE: earlier partial runs left 2 extra uncancelled orders on the test account
(the glimpse pagination showed 3 dots); they can be cancelled by hand.
[2026-09-14 17:30:00] E2E: drive admin-ui (create slot + product, attach) then order it in user-ui.
Goal: one round that sets up test data in the admin app and consumes it in the
user app.
apps/admin-ui/app/(drawer)/dashboard/slots/slot-details.tsx — the Edit-slot FAB
had no testID, so the flow can't target it on a screen with other controls:
<MyTouchableOpacity
+ testID="edit-slot-fab"
onPress={() => router.push(`/(drawer)/dashboard/slots/edit/${slot.id}` as any)}
New flow files (appId in.freshyo.adminui):
- e2e/flows/admin-slot.yaml login if needed -> Dashboard -> Delivery Slots
-> add-slot-fab -> Create New Slot. The date/time pickers are native Material
dialogs; they are driven through their Android resource-ids
(android:id/next, button1/button2, toggle_mode, input_hour, input_minute) with
the clock dialog switched to "Type in time" via `notVisible: "Type in time"`.
Delivery = 15th of next month 18:00, freeze same day 17:00 (always future).
- e2e/flows/admin-product.yaml login if needed -> Dashboard -> "Add Product"
quick action -> fill name/price/quantity -> Create Product.
(ProductForm has no testIDs; fields are addressed by label/placeholder.)
- e2e/flows/admin-attach-product.yaml login if needed -> Delivery Slots -> find
the slot by its delivery label (scrollUntilVisible) -> slot row -> Slot Details
-> edit-slot-fab -> "Select products for this slot" -> search the product ->
select -> Done -> Update Slot.
New flow + subflows (appId in.freshyo.app):
- e2e/flows/user-search-order.yaml home refresh -> SearchBar -> search the new
product -> add from the result card -> shared checkout/place-order tail ->
home glimpse -> cancel (keeps the test account clean).
e2e/run.sh — generate and forward the cross-flow values as -e flags:
+ SLOT_DATE_DESC="$(date -v+1m -v15d '+%d %B %Y')" # "15 October 2026"
+ SLOT_DELIVERY_LABEL="$(date -v+1m -v15d '+%d %b'), 6:00 PM"
+ PRODUCT_NAME="E2E Test Item $(date '+%d%b-%H%M')" # unique per run
+ PRODUCT_PRICE="499"
plus ADMIN_NAME / ADMIN_PASSWORD from e2e/.env.
e2e/.env.example — document ADMIN_NAME / ADMIN_PASSWORD.
[2026-09-14 19:45:00] E2E: full product lifecycle across admin-ui + user-ui.
New flow file e2e/flows/admin-product-full.yaml (appId in.freshyo.adminui, and it
launches the user app inline to verify each change — Maestro lets launchApp take
an appId, so one flow can drive both apps and interleave them).
Scope (per product, created fresh each run so real catalog data is untouched):
1. create + attach to the slot (reuses admin-product.yaml, admin-attach-product.yaml)
2. user app: search it, assert the unit/price on the card
3. admin: Suspend SKU, change the quantity attribute (units), toggle Flash
Available off, Add Variant (multi-SKU), replace the image, Save Changes
4. user app: assert each effect (suspended -> Unavailable, new unit on the card,
no "1 hr Delivery" row in the add dialog)
Verified while scoping this (all on emulator-5554, product edit form):
- The variant card exposes everything needed: the quantity attribute inputs
(name/value), Market/Our Price, the Flash Available / Offer SKU / Combo Only SKU
/ Suspend SKU checkboxes, the image add tile, "+ Add Variant" and "Save Changes".
- Image upload: apps/admin-ui ImageUploaderNeo -> expo-image-picker
launchImageLibraryAsync -> the Android system Photo Picker. Nothing in-app can
set an image, so the flow pushes a fixture image to the device and drives the
picker:
adb push <repo image> /sdcard/Pictures/e2e-test-image.png
+ a MEDIA_SCANNER_SCAN_FILE broadcast so it shows under "Recent"
The picker opens with Photos/Albums tabs and a multi-select grid (tiles have
selection circles), so it is drivable by tapping the first tile.
NOT YET RUN: the flow file below is written from the verified selectors of the
existing admin flows but has not been executed end to end yet.
[2026-09-14 20:10:00] Fix: slot products dropdown is only addressable by text.
Symptom: "Element not found: Text matching regex: Select products for this slot"
when re-attaching a product to a slot that already has one. The trigger renders
the SELECTED labels instead of the placeholder, and ProductsSelector was rendered
without a testID, so the trigger, its search box and its Done button all came out
as `undefined`, `undefined-search`, `undefined-done` — nothing stable to target.
apps/admin-ui/components/ProductsSelector.tsx — accept and forward a testID to the
product (not group) dropdown:
interface ProductsSelectorProps {
+ testID?: string;
groups?: Group[];
...
export default function ProductsSelector({
value,
+ testID,
...
<BottomDropdown
+ testID={testID}
label={label}
apps/admin-ui/components/SlotForm.tsx — name the two selectors:
label="Select Products"
+ testID="slot-products-selector"
placeholder="Select products for this slot"
...
label="Select Products"
+ testID="snippet-products-selector"
placeholder="Select products for snippet"
e2e/flows/admin-attach-product.yaml — tap the trigger by id instead of by its
placeholder text:
+- # The trigger shows the selected labels once the slot has products, so target
+- # it by id rather than by its placeholder text.
- tapOn: "Select products for this slot"
+ - tapOn:
+ id: "slot-products-selector"
e2e/flows/admin-product-full.yaml — same change for the attach leg.

14
e2e/.env.example Normal file
View file

@ -0,0 +1,14 @@
# Copy this file to e2e/.env (gitignored). `run.sh` / `bun run e2e` loads it
# automatically and forwards the values to Maestro as -e flags.
#
# Login uses the username + password path ("other ways to login"), because the
# OTP flow needs a real SMS/OTP that can't be automated.
TEST_IDENTIFIER=
TEST_PASSWORD=
# Optional: the address vars already have non-secret defaults in config.yaml.
# Set them here only if you want to override those.
# ADDRESS_NAME=
# ADDRESS_PHONE=
# ADDRESS_LINE1=

125
e2e/README.md Normal file
View file

@ -0,0 +1,125 @@
# Maestro E2E — apps/user-ui (Android only)
Maestro flows that drive the **user-ui** app through placing an order:
home → product → add to cart (choose delivery slot) → cart → checkout
(address if needed, Cash on Delivery) → **Place Order** → confirmation.
Everything here targets **Android only** (`appId: in.freshyo.app`), and `run.sh`
always passes `--platform android` — it will never use an iOS simulator.
## Prerequisites
- Maestro CLI installed (`maestro --version`; installed here at `~/.maestro/bin/maestro`).
- A running **Android emulator/device** with the user-ui dev build installed:
```bash
cd apps/user-ui
npx expo run:android
```
- A **test account with a password set**. OTP login can't be automated (the OTP
is server-issued, no dev bypass). The flow reaches the login screen by tapping
**Checkout** as a guest — checkout is behind `useAuthenticatedRoute`, so the app
bounces there — then signs in via `other ways to login``Username` +
`Password` and returns to the cart. Alternatively, sign in once by hand and the
subflow becomes a no-op; `launchApp` uses `clearState: false` so the session and
the first-run onboarding survive.
- A catalog with at least one **in-stock product that has a future delivery slot**.
Note: in a dev build the "Open debugger to view warnings" toast overlays the bottom
of the screen and swallows taps on the bottom tab bar — the flows avoid the tab bar
and use `centerElement: true` where a control can land under it.
## Running
**Recommended — one command, values from a gitignored file.** Create `e2e/.env`
(see `.env.example`) and then, from the repo root:
```bash
bun run e2e # or: bash e2e/run.sh
```
`e2e/run.sh` sources `e2e/.env`, runs `maestro test e2e --platform android`, and
falls back to `~/.maestro/bin/maestro` when `maestro` isn't on `PATH`. Extra flags
pass through, and flow paths are relative to `e2e/`:
```bash
bash e2e/run.sh flows/place-order.yaml # one flow
bash e2e/run.sh --format junit # junit report
bash e2e/run.sh --device emulator-5554 # pick an emulator (needed if several are connected)
```
To see which emulators are available: `adb devices -l`.
### How values are supplied
Maestro resolves `${VAR}` from three places:
1. **`config.yaml``env:`** — committed, non-secret defaults shared by all flows
(`ADDRESS_NAME`, `ADDRESS_PHONE`, `ADDRESS_LINE1` live here).
2. **`e2e/.env`** (gitignored) — loaded by `run.sh`; put `TEST_IDENTIFIER` /
`TEST_PASSWORD` here so secrets stay out of git and off the command line.
3. **Shell / CLI**`MAESTRO_`-prefixed env vars or `-e KEY=VALUE`, good for CI:
```bash
MAESTRO_TEST_IDENTIFIER=... MAESTRO_TEST_PASSWORD=... bash e2e/run.sh
# or
maestro test e2e -e TEST_IDENTIFIER=... -e TEST_PASSWORD=...
```
Need more than one setup (e.g. CI vs local)? Maestro supports alternate configs:
`maestro test --config e2e/ci-config.yaml e2e`.
Useful flags: `--include-tags`, `--format junit`, `--test-output-dir`.
Reports/screenshots land in `e2e/.maestro-output/`.
## Layout
```
e2e/
config.yaml # workspace config (flows, execution order, env defaults)
subflows/login.yaml # reusable login (runs only if the login screen is visible)
flows/smoke.yaml # app launches, home renders
flows/place-order.yaml # the order flow
```
## What place-order covers
1. Home → add a product from the card's cart icon → pick a delivery slot in the
"Select Delivery Slot" dialog → add to cart.
(The icon sits inside the card's touchable, so the tap can fall through to the
card; the flow then adds from the product detail page instead.)
2. Cart → checkout. A guest is bounced to login (checkout is behind
`useAuthenticatedRoute`), signs in, and the flow re-enters the cart.
3. Address (only when the account has none) → Cash on Delivery → **Place Order**.
4. **Home glimpse**: `Continue Shopping` returns home; the flow scrolls to the
`NextOrderGlimpse` card and captures it with `takeScreenshot`, so each run
leaves a picture of the upcoming-order card. Maestro writes it under
`~/.maestro/tests/<timestamp>/place-order/takeScreenshot/home-upcoming-order-glimpse.png`.
5. **Cancel**: taps the glimpse card (which routes to
`/(drawer)/(tabs)/me/my-orders/{id}`), scrolls to **Cancel Order**, fills the
reason, confirms, dismisses the success alert, and asserts the
**Cancellation Reason** panel renders.
## Notes
- **These flows place real orders** on whatever backend the app points at, and
then cancel the one they placed. Point the app at a non-production backend /
use a throwaway account.
- The product is chosen as **the first card** (`id: product-card`, `index: 0`).
If that item is out of stock or has no future slot, change the index or seed a
known product.
- The address step is conditional: it only runs when `No addresses found` is
visible, so an account with a saved address skips it.
- Payment defaults to **Cash on Delivery**, so the flow just asserts it's visible.
- Controls below the fold need `scrollUntilVisible`; the home glimpse sits below
**Our Stores**, and **Cancel Order** below the bill summary.
- `launchApp` uses `clearState: false`, so the session and (on the first run) the
onboarding survive. The trade-off is that the app resumes wherever the previous
run left it — the flow taps the **Home** tab (`optional: true`) to get back.
- Maestro taps can be eaten by a dismissing keyboard or the dev-warning toast, so
the flow waits for animations and retries the confirm tap once.
- Stable `testID` anchors used by the flows live in user-ui:
`product-card`, `add-to-cart-icon`, `slot-option`, `add-to-cart-confirm`,
`go-to-cart`, `checkout-button`, `address-name`, `address-phone`,
`address-line1`, `address-submit`, `place-order-button`, `cancel-reason`.

21
e2e/config.yaml Normal file
View file

@ -0,0 +1,21 @@
# Maestro workspace configuration for apps/user-ui E2E tests (Android only).
# Run from the repo root: bash e2e/run.sh (or: bun run e2e)
flows:
- 'flows/**'
executionOrder:
# Stop the suite as soon as a flow fails.
continueOnFailure: false
flowsOrder:
- smoke
- place-order
# Non-secret defaults shared by all flows. ${VAR} in a flow's commands resolves
# from here. Secrets (TEST_IDENTIFIER / TEST_PASSWORD) belong in the gitignored
# e2e/.env — see e2e/run.sh — not in this committed file.
env:
ADDRESS_NAME: Test User
ADDRESS_PHONE: '9876543210'
ADDRESS_LINE1: '1 Test Street'
testOutputDir: .maestro-output

View file

@ -0,0 +1,92 @@
# Admin: attach the product created in admin-product.yaml to the slot created in
# admin-slot.yaml.
#
# The slot is found by its delivery label (SLOT_DELIVERY_LABEL) rather than by
# id, because the id is only known at runtime. Tapping the row opens Slot
# Details; the edit FAB there reopens the same SlotForm used to create it.
#
# Env (see e2e/run.sh): ADMIN_NAME, ADMIN_PASSWORD, PRODUCT_NAME,
# SLOT_DELIVERY_LABEL (e.g. "15 Oct, 6:00 PM")
appId: in.freshyo.adminui
---
- launchApp:
clearState: false
- runFlow: ../subflows/dismiss-dev-toast.yaml
- runFlow: ../subflows/admin-login.yaml
- extendedWaitUntil:
visible: "Dashboard"
timeout: 90000
# --- Slots list: find our slot ----------------------------------------------
- tapOn:
id: "delivery-slots-menu-item"
- extendedWaitUntil:
visible:
id: "add-slot-fab"
timeout: 40000
- runFlow:
when:
notVisible: ${SLOT_DELIVERY_LABEL}
commands:
- scrollUntilVisible:
element:
text: ${SLOT_DELIVERY_LABEL}
direction: DOWN
centerElement: true
timeout: 40000
- tapOn: ${SLOT_DELIVERY_LABEL}
# The details screen keeps the "Slots" header and titles itself "Slot #<id>",
# so anchor on a section that only exists there.
- extendedWaitUntil:
visible: "Vendor Snippets"
timeout: 40000
# --- Slot Details -> edit form ----------------------------------------------
- runFlow: ../subflows/dismiss-dev-toast.yaml
- tapOn:
id: "edit-slot-fab"
- extendedWaitUntil:
visible: "Edit Slot"
timeout: 40000
# --- Attach the product in the multi-select dropdown ------------------------
# Target the trigger by id: once the slot has any products it renders the
# selected labels instead of its placeholder text.
- tapOn:
id: "slot-products-selector"
- extendedWaitUntil:
visible: "Select Products"
timeout: 30000
- tapOn:
id: "slot-products-selector-search"
- inputText: ${PRODUCT_NAME}
- hideKeyboard
- waitForAnimationToEnd:
timeout: 5000
# The option label is the SKU label — "<product name> <feature values>". The
# pattern must require that trailing part: it stops it matching the search input
# itself (which holds exactly the product name), and tapping the input just
# re-opens the keyboard instead of selecting the option.
- extendedWaitUntil:
visible: "${PRODUCT_NAME} .+"
timeout: 30000
- tapOn: "${PRODUCT_NAME} .+"
- waitForAnimationToEnd:
timeout: 3000
- tapOn:
id: "slot-products-selector-done"
# --- Save -------------------------------------------------------------------
- scrollUntilVisible:
element:
id: "create-slot-button"
direction: DOWN
centerElement: true
timeout: 30000
- runFlow: ../subflows/dismiss-dev-toast.yaml
- tapOn:
id: "create-slot-button"
- extendedWaitUntil:
visible: "Slot updated successfully!"
timeout: 40000
- tapOn: "OK"

View file

@ -0,0 +1,243 @@
# Full product lifecycle, driven across BOTH apps in one run.
#
# Maestro's launchApp accepts an appId, so a single flow can alternate between
# the admin app and the user app and verify each change immediately.
#
# Coverage:
# - create a product + attach it to the slot (admin)
# - it is searchable and orderable (user)
# - suspend the SKU (admin) -> user: Unavailable
# - change its units (quantity attribute) (admin) -> user: new unit
# - flash: enable, then disable (admin) -> user: no 1 hr row
# - add a second variant (multiple SKUs) (admin) -> user: variant count
# - replace the image (admin)
# - save and re-verify (user)
#
# Env (see e2e/run.sh): ADMIN_NAME, ADMIN_PASSWORD, PRODUCT_NAME, PRODUCT_PRICE,
# TEST_IDENTIFIER, TEST_PASSWORD, SLOT_DELIVERY_LABEL
#
# NOTE: the image step drives the Android system Photo Picker, which only lists
# photos already on the device — e2e/run.sh pushes a fixture image first.
appId: in.freshyo.adminui
---
# --- 1. Admin: create the product and attach it to the slot ------------------
# Reuses the standalone flows so the selectors live in one place.
- runFlow: admin-product.yaml
- runFlow: admin-attach-product.yaml
# --- 2. User: the new product is searchable ---------------------------------
- launchApp:
appId: in.freshyo.app
clearState: false
- tapOn:
text: "^Home$"
optional: true
- extendedWaitUntil:
visible:
id: "add-to-cart-icon"
timeout: 90000
# The catalog is a versioned CDN file, so refresh to pick up the new product.
- swipe:
start: "50%,30%"
end: "50%,80%"
- waitForAnimationToEnd:
timeout: 10000
- tapOn: "Search fresh meat..."
- extendedWaitUntil:
visible: "All Products"
timeout: 30000
- inputText: ${PRODUCT_NAME}
- hideKeyboard
- extendedWaitUntil:
visible: "${PRODUCT_NAME}.*"
timeout: 120000
# The card carries the unit notation and the price from the admin form.
- assertVisible: "Quantity: 0.5 kg"
# --- 3. Admin: suspend the SKU, change its units, add a variant -------------
- launchApp:
appId: in.freshyo.adminui
clearState: false
- extendedWaitUntil:
visible: "Dashboard"
timeout: 90000
- tapOn: "Products"
- extendedWaitUntil:
visible: "Search products..."
timeout: 40000
- tapOn: "Search products..."
- inputText: ${PRODUCT_NAME}
- hideKeyboard
- waitForAnimationToEnd:
timeout: 5000
- tapOn: "Edit"
- extendedWaitUntil:
visible: "Enter product name"
timeout: 40000
# Units: the quantity attribute value (auto-added, name is locked to "quantity").
# Scroll to it first — centering the checkbox labels below would push it off-screen.
- scrollUntilVisible:
element:
text: "0.5 kg"
direction: DOWN
centerElement: true
timeout: 30000
- tapOn: "0.5 kg"
- eraseText
- inputText: "1 kg"
- hideKeyboard
# Multi-SKU: a second variant with its own price and unit. The leading "+" is an
# icon rather than part of the label, so match on the label alone.
- scrollUntilVisible:
element:
text: ".*Add Variant"
direction: DOWN
centerElement: true
timeout: 30000
- tapOn: ".*Add Variant"
- waitForAnimationToEnd:
timeout: 5000
- scrollUntilVisible:
element:
text: "Variant 2"
direction: DOWN
centerElement: true
timeout: 30000
- waitForAnimationToEnd:
timeout: 5000
- scrollUntilVisible:
element:
text: "Selling price"
direction: DOWN
centerElement: true
timeout: 30000
# Settle before tapping: a tap that lands while the list is still scrolling hits
# nothing, which is why this field stayed empty before.
- waitForAnimationToEnd:
timeout: 5000
- tapOn: "Selling price"
- inputText: "999"
- hideKeyboard
- scrollUntilVisible:
element:
text: "e.g. 0.5 kg"
direction: DOWN
centerElement: true
timeout: 30000
# Variant 1's price/value are already filled, so only variant 2's empty fields
# render these placeholders — exactly one match each, hence no index.
- waitForAnimationToEnd:
timeout: 5000
- tapOn: "e.g. 0.5 kg"
- inputText: "2 kg"
- hideKeyboard
# Flash: turn it on, fill the flash price, then turn it back off.
- scrollUntilVisible:
element:
text: "Flash Available"
direction: DOWN
centerElement: true
timeout: 30000
- tapOn: "Flash Available"
- waitForAnimationToEnd:
timeout: 3000
- runFlow:
when:
visible: "Enter flash price"
commands:
- tapOn: "Enter flash price"
- inputText: "399"
- hideKeyboard
- tapOn: "Flash Available"
- waitForAnimationToEnd:
timeout: 3000
# Suspend the SKU (the whole product effectively goes unavailable to users).
- tapOn: "Suspend SKU"
# Replace the image: the add tile sits just below the checkboxes, and this is the
# only path (expo-image-picker -> Android Photo Picker).
- tapOn:
point: "21%,61%"
- waitForAnimationToEnd:
timeout: 8000
- runFlow:
when:
visible: "This app can only access the photos you select"
commands:
- tapOn:
point: "16%,32%"
- waitForAnimationToEnd:
timeout: 3000
# The confirm button is labelled "Add (1)" once a photo is selected, so it
# can't be matched by a bare "Add".
- runFlow:
when:
visible: "Add.*"
commands:
- tapOn: "Add.*"
- runFlow:
when:
visible: "Done"
commands:
- tapOn: "Done"
# Save everything.
- scrollUntilVisible:
element:
text: "Save Changes"
direction: DOWN
centerElement: true
timeout: 30000
- waitForAnimationToEnd:
timeout: 5000
- tapOn: "Save Changes"
# Retry while the button is still up: a tap that lands mid-settle is swallowed,
# and the label only goes away once the mutation is actually in flight.
- repeat:
times: 5
commands:
- runFlow:
when:
visible: "Save Changes"
commands:
- waitForAnimationToEnd:
timeout: 3000
- tapOn: "Save Changes"
- extendedWaitUntil:
visible: "Product updated successfully!"
timeout: 60000
- tapOn: "OK"
# --- 4. User: the changes are visible ---------------------------------------
- launchApp:
appId: in.freshyo.app
clearState: false
- tapOn:
text: "^Home$"
optional: true
- extendedWaitUntil:
visible:
id: "add-to-cart-icon"
timeout: 90000
- swipe:
start: "50%,30%"
end: "50%,80%"
- waitForAnimationToEnd:
timeout: 10000
- tapOn: "Search fresh meat..."
- extendedWaitUntil:
visible: "All Products"
timeout: 30000
- inputText: ${PRODUCT_NAME}
- hideKeyboard
# Suspended SKU -> the card is marked out of stock and cannot be added.
- extendedWaitUntil:
visible: "${PRODUCT_NAME}.*"
timeout: 120000
- assertVisible: "Out of Stock"
- assertVisible: "UNAVAILABLE"

View file

@ -0,0 +1,64 @@
# Admin: create a product (one item variant) so the user app can order it.
#
# ProductForm has no testIDs, so fields are addressed by their label/placeholder:
# "Enter product name" -> name (must be globally unique)
# "Selling price" -> Our Price (must be > 0)
# "e.g. 0.5 kg" -> quantity feature value (required)
# Store and Product Type are pre-filled ("1" / "Item"), and images are optional,
# so nothing else is needed for a valid save.
#
# Env (see e2e/run.sh): ADMIN_NAME, ADMIN_PASSWORD, PRODUCT_NAME, PRODUCT_PRICE
appId: in.freshyo.adminui
---
- launchApp:
clearState: false
- runFlow: ../subflows/dismiss-dev-toast.yaml
- runFlow: ../subflows/admin-login.yaml
- extendedWaitUntil:
visible: "Dashboard"
timeout: 90000
# --- Dashboard quick action -> product form ---------------------------------
- tapOn: "Add Product"
# The form is long, so anchor on its first field (the submit button is far below
# the fold and would fail an assertVisible).
- extendedWaitUntil:
visible: "Enter product name"
timeout: 60000
# --- Required fields --------------------------------------------------------
- tapOn: "Enter product name"
- inputText: ${PRODUCT_NAME}
- hideKeyboard
- scrollUntilVisible:
element:
text: "Selling price"
direction: DOWN
centerElement: true
timeout: 30000
- tapOn: "Selling price"
- inputText: ${PRODUCT_PRICE}
- hideKeyboard
- scrollUntilVisible:
element:
text: "e.g. 0.5 kg"
direction: DOWN
centerElement: true
timeout: 30000
- tapOn: "e.g. 0.5 kg"
- inputText: "0.5 kg"
- hideKeyboard
# --- Submit -----------------------------------------------------------------
- scrollUntilVisible:
element:
text: "Create Product"
direction: DOWN
centerElement: true
timeout: 30000
- runFlow: ../subflows/dismiss-dev-toast.yaml
- tapOn: "Create Product"
- extendedWaitUntil:
visible: "Product created successfully!"
timeout: 60000
- tapOn: "OK"

103
e2e/flows/admin-slot.yaml Normal file
View file

@ -0,0 +1,103 @@
# Admin: create a delivery slot with a future date (no products yet — the
# product is attached in admin-attach-product.yaml).
#
# The date/time pickers are native Android Material dialogs, so they are driven
# through their resource-ids rather than text:
# android:id/next next month
# android:id/button1 OK (button2 = CANCEL)
# android:id/toggle_mode switch the clock dialog to text entry
# android:id/input_hour / input_minute the typed time fields
# The clock dialog remembers its last mode, hence the `notVisible` guard.
#
# Env (see e2e/run.sh): ADMIN_NAME, ADMIN_PASSWORD, SLOT_DATE_DESC
# SLOT_DATE_DESC = the 15th of next month, e.g. "15 October 2026"
appId: in.freshyo.adminui
---
- launchApp:
clearState: false
- runFlow: ../subflows/dismiss-dev-toast.yaml
- runFlow: ../subflows/admin-login.yaml
- extendedWaitUntil:
visible: "Dashboard"
timeout: 90000
# --- Slots list -> create form ----------------------------------------------
- tapOn:
id: "delivery-slots-menu-item"
- extendedWaitUntil:
visible:
id: "add-slot-fab"
timeout: 40000
- runFlow: ../subflows/dismiss-dev-toast.yaml
- tapOn:
id: "add-slot-fab"
- extendedWaitUntil:
visible: "Create New Slot"
timeout: 40000
# --- Delivery date & time ---------------------------------------------------
- tapOn:
id: "delivery-date-picker"
- tapOn:
id: "android:id/next"
- tapOn: ${SLOT_DATE_DESC}
- tapOn:
id: "android:id/button1"
- tapOn:
id: "delivery-time-picker"
- runFlow:
when:
notVisible: "Type in time"
commands:
- tapOn:
id: "android:id/toggle_mode"
- tapOn:
id: "android:id/input_hour"
- eraseText
- inputText: "18"
- tapOn:
id: "android:id/input_minute"
- eraseText
- inputText: "00"
- tapOn:
id: "android:id/button1"
# --- Freeze date & time (must be <= delivery) -------------------------------
- tapOn:
id: "freeze-date-picker"
- tapOn:
id: "android:id/next"
- tapOn: ${SLOT_DATE_DESC}
- tapOn:
id: "android:id/button1"
- tapOn:
id: "freeze-time-picker"
- runFlow:
when:
notVisible: "Type in time"
commands:
- tapOn:
id: "android:id/toggle_mode"
- tapOn:
id: "android:id/input_hour"
- eraseText
- inputText: "17"
- tapOn:
id: "android:id/input_minute"
- eraseText
- inputText: "00"
- tapOn:
id: "android:id/button1"
# --- Submit -----------------------------------------------------------------
- runFlow: ../subflows/dismiss-dev-toast.yaml
- tapOn:
id: "create-slot-button"
- extendedWaitUntil:
visible: "Slot created successfully!"
timeout: 40000
- tapOn: "OK"
- extendedWaitUntil:
visible:
id: "add-slot-fab"
timeout: 40000

View file

@ -0,0 +1,65 @@
# End-to-end: place an order (scheduled delivery, Cash on Delivery).
#
# Preconditions:
# - a running Android emulator with the user-ui dev build installed
# - a test account that has a password set (see e2e/README.md)
# - the catalog has at least one in-stock product with a future delivery slot
#
# Env (set in e2e/.env, see e2e/.env.example):
# TEST_IDENTIFIER, TEST_PASSWORD
# ADDRESS_NAME, ADDRESS_PHONE, ADDRESS_LINE1 (used only when no address exists)
#
# Android-only suite (Android package id).
appId: in.freshyo.app
---
- launchApp:
clearState: false
# The app can resume on whatever screen the previous run left open (product
# details, checkout, an order, ...). All of those still render the bottom tab
# bar, so tapping the Home tab is a reliable way back to the grid.
- tapOn:
text: "^Home$"
optional: true
# --- Home is ready ----------------------------------------------------------
# Anchor on the add button itself (present on every card that isn't already in
# the cart), so a cart carried over from a previous run doesn't matter.
- extendedWaitUntil:
visible:
id: "add-to-cart-icon"
timeout: 90000
# --- Add a product straight from the home card ------------------------------
# Use the card's cart icon instead of opening the product detail screen; on home
# that icon opens the Add-to-Cart dialog (useAddToCartDialog). The icon sits
# inside the card's touchable, so the tap can occasionally fall through to the
# card — when that happens we're on the detail page and add from there instead.
- tapOn:
id: "add-to-cart-icon"
index: 0
- runFlow:
when:
visible: "Product Details"
commands:
- extendedWaitUntil:
visible: "Add to Cart"
timeout: 30000
- tapOn: "Add to Cart"
# --- Choose a delivery slot in the dialog, then add -------------------------
- extendedWaitUntil:
visible: "Select Delivery Slot"
timeout: 30000
- tapOn:
id: "slot-option"
index: 0
- tapOn:
id: "add-to-cart-confirm"
# --- Cart -> checkout -> place -> glimpse -> cancel -------------------------
# Shared with user-search-order.yaml so both order paths stay in step.
- tapOn:
id: "go-to-cart"
- runFlow: ../subflows/checkout-and-place-order.yaml
- runFlow: ../subflows/glimpse-and-cancel.yaml

13
e2e/flows/smoke.yaml Normal file
View file

@ -0,0 +1,13 @@
# Smoke test: the app launches and the home screen renders (no auth needed —
# home is browsable while logged out).
# Android-only suite (Android package id).
appId: in.freshyo.app
---
- launchApp:
clearState: false
# Home has rendered when a product card is on screen. Dev builds are slow to
# cold-start (Metro bundle), so allow plenty of time.
- extendedWaitUntil:
visible:
id: "product-card"
timeout: 60000

View file

@ -0,0 +1,70 @@
# User app: search for the product the admin flow just created, add it to the
# cart, and order it (then cancel, so the round leaves no live orders).
#
# The catalog the app searches comes from a versioned products.json on the CDN
# (`/products/v-<cacheVersion>/products.json`), and the backend bumps that
# version when a product is created, so the app only sees it after a refresh —
# hence the pull-to-refresh and the generous wait below.
#
# Env (see e2e/run.sh): PRODUCT_NAME, TEST_IDENTIFIER, TEST_PASSWORD, ADDRESS_*
appId: in.freshyo.app
---
- launchApp:
clearState: false
# The app resumes wherever the last run left it; the Home tab is always there.
- tapOn:
text: "^Home$"
optional: true
- extendedWaitUntil:
visible:
id: "add-to-cart-icon"
timeout: 90000
# --- Pull to refresh so the newly created product is in the catalog ---------
- swipe:
start: "50%,30%"
end: "50%,80%"
- waitForAnimationToEnd:
timeout: 10000
- extendedWaitUntil:
visible:
id: "add-to-cart-icon"
timeout: 60000
# --- Search ------------------------------------------------------------------
# The home search bar is not editable — tapping it opens the search screen.
- tapOn: "Search fresh meat..."
- extendedWaitUntil:
visible: "All Products"
timeout: 30000
- inputText: ${PRODUCT_NAME}
- hideKeyboard
- extendedWaitUntil:
visible: "${PRODUCT_NAME}.*"
timeout: 120000
# --- Add the result to the cart ---------------------------------------------
# Search results render the full-width "Add to Cart" button on the card (they
# don't use the small cart icon the home grid uses). An item already in the cart
# shows a quantifier instead, so the add is conditional — a cart carried over
# from a previous run still ends up ordered.
- runFlow:
when:
visible: "Add to Cart"
commands:
- tapOn: "Add to Cart"
- extendedWaitUntil:
visible: "Select Delivery Slot"
timeout: 30000
- tapOn:
id: "slot-option"
index: 0
- tapOn:
id: "add-to-cart-confirm"
- tapOn:
id: "go-to-cart"
index: 0
# --- Checkout -> place -> glimpse -> cancel ---------------------------------
- runFlow: ../subflows/checkout-and-place-order.yaml
- runFlow: ../subflows/glimpse-and-cancel.yaml

77
e2e/run.sh Normal file
View file

@ -0,0 +1,77 @@
#!/usr/bin/env bash
#
# Runs the Maestro E2E suite for apps/user-ui — Android only.
#
# - loads secrets/defaults from the gitignored e2e/.env (Maestro has no .env support)
# - always passes --platform android, so it can never target an iOS simulator
# - falls back to ~/.maestro/bin/maestro when `maestro` isn't on PATH
#
# Usage:
# bash e2e/run.sh # all flows
# bash e2e/run.sh flows/place-order.yaml # a single flow
# bash e2e/run.sh --device emulator-5554 # target a specific emulator
# bash e2e/run.sh --format junit # extra Maestro flags pass through
set -euo pipefail
E2E_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="$E2E_DIR/.env"
if [ -f "$ENV_FILE" ]; then
# `set -a` exports every variable defined while sourcing.
set -a
# shellcheck disable=SC1090
. "$ENV_FILE"
set +a
else
echo "warning: $ENV_FILE not found — copy e2e/.env.example to e2e/.env and fill it in." >&2
fi
MAESTRO_BIN="${MAESTRO_BIN:-maestro}"
if ! command -v "$MAESTRO_BIN" >/dev/null 2>&1; then
MAESTRO_BIN="$HOME/.maestro/bin/maestro"
fi
# Allow flow paths relative to e2e/ (e.g. `flows/place-order.yaml`) while
# running from the repo root. Anything else is forwarded to Maestro as-is.
TARGET="$E2E_DIR"
if [ "$#" -gt 0 ]; then
case "$1" in
-*) : ;;
*)
if [ -e "$E2E_DIR/$1" ]; then
TARGET="$E2E_DIR/$1"
shift
fi
;;
esac
fi
# Values shared across the admin + user flows in one round. The slot is always
# created for the 15th of next month, so it is in the future whatever time the
# suite runs at (the admin pickers are driven with SLOT_DATE_DESC).
SLOT_DATE_DESC="$(date -v+1m -v15d '+%d %B %Y')"
SLOT_DELIVERY_LABEL="$(date -v+1m -v15d '+%d %b'), 6:00 PM"
PRODUCT_NAME="${PRODUCT_NAME:-E2E Test Item $(date '+%d%b-%H%M')}"
PRODUCT_PRICE="${PRODUCT_PRICE:-499}"
# Maestro doesn't read the shell environment — values from .env have to be
# forwarded explicitly as `-e KEY=VALUE`.
ENV_ARGS=()
for key in TEST_IDENTIFIER TEST_PASSWORD ADDRESS_NAME ADDRESS_PHONE ADDRESS_LINE1 \
ADMIN_NAME ADMIN_PASSWORD PRODUCT_NAME PRODUCT_PRICE \
SLOT_DATE_DESC SLOT_DELIVERY_LABEL; do
if [ -n "${!key:-}" ]; then
ENV_ARGS+=(-e "$key=${!key}")
fi
done
# Fixture photo for the product-image step. The admin app can only set a product
# image through expo-image-picker -> the Android Photo Picker, which lists photos
# already on the device, so make sure one is there (idempotent).
FIXTURE_SRC="$E2E_DIR/../apps/user-ui/assets/images/icon.png"
if [ -f "$FIXTURE_SRC" ] && adb devices 2>/dev/null | grep -q "device$"; then
adb push "$FIXTURE_SRC" /sdcard/Pictures/e2e-test-image.png >/dev/null 2>&1 || true
adb shell "am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE -d file:///sdcard/Pictures/e2e-test-image.png" >/dev/null 2>&1 || true
fi
exec "$MAESTRO_BIN" test "$TARGET" --platform android ${ENV_ARGS[@]+"${ENV_ARGS[@]}"} "$@"

View file

@ -0,0 +1,25 @@
# Reusable admin login subflow.
#
# Only acts when the admin login screen is actually showing, so an existing
# staff session (launchApp without clearState) is left untouched.
#
# Env (from e2e/.env): ADMIN_NAME, ADMIN_PASSWORD
appId: in.freshyo.adminui
---
- runFlow:
when:
visible: "Admin Login"
commands:
- tapOn:
id: "login-name-input"
- inputText: ${ADMIN_NAME}
- tapOn:
id: "login-password-input"
- inputText: ${ADMIN_PASSWORD}
- hideKeyboard
- tapOn:
id: "login-button"
# The screen title disappears once we land on the dashboard.
- extendedWaitUntil:
notVisible: "Admin Login"
timeout: 60000

View file

@ -0,0 +1,96 @@
# Places whatever is in the cart, from the cart page through to the success
# screen. Shared by place-order.yaml (add from home) and user-search-order.yaml
# (add from search).
#
# Starts on the cart page and ends on "Order Placed Successfully!".
appId: in.freshyo.app
---
- assertVisible: "Scheduled Delivery Cart"
# The Checkout button sits under the bill details, below the fold.
- scrollUntilVisible:
element:
id: "checkout-button"
direction: DOWN
- tapOn:
id: "checkout-button"
# --- Sign in ---------------------------------------------------------------
# Checkout is behind useAuthenticatedRoute, so a guest is bounced to the login
# screen here. Let the redirect settle before the conditional check, otherwise
# the login screen hasn't rendered yet and the subflow is skipped.
# login.yaml is a no-op when a session already exists (OTP login can't be
# automated, so a password must be set on the test account).
- waitForAnimationToEnd:
timeout: 15000
- runFlow: login.yaml
# Login lands back on Home, so once we're authenticated head to the cart again.
# (Skipped when a session already existed and we're still on the checkout page.)
- runFlow:
when:
visible:
id: "add-to-cart-icon"
commands:
- tapOn:
id: "go-to-cart"
- scrollUntilVisible:
element:
id: "checkout-button"
direction: DOWN
- tapOn:
id: "checkout-button"
- extendedWaitUntil:
visible: "Scheduled Delivery Checkout"
timeout: 45000
# --- Add an address only if the account has none ----------------------------
# The selector renders "No addresses found" both when the account has none and
# while the addresses query is still loading, so settle and check again before
# creating one — otherwise we open the add-address form for an account that
# already has an address.
- runFlow:
when:
visible: "No addresses found"
commands:
- waitForAnimationToEnd:
timeout: 10000
- runFlow:
when:
visible: "No addresses found"
commands:
- tapOn: "Add Address"
- tapOn:
id: "address-name"
- inputText: ${ADDRESS_NAME}
- tapOn:
id: "address-phone"
- inputText: ${ADDRESS_PHONE}
- tapOn:
id: "address-line1"
- inputText: ${ADDRESS_LINE1}
- hideKeyboard
- tapOn:
id: "address-submit"
- assertVisible: "Delivery Address"
# --- Pay (COD is preselected) and place the order ---------------------------
# The payment section sits below the address block; scrolling also lifts the
# button clear of the dev-warning toast that overlays the bottom of the screen.
- scrollUntilVisible:
element:
text: "Cash on Delivery"
direction: DOWN
- assertVisible: "Cash on Delivery"
- scrollUntilVisible:
element:
id: "place-order-button"
direction: DOWN
centerElement: true
- tapOn:
id: "place-order-button"
# --- Confirmation -----------------------------------------------------------
# The checkout shows a "Placing your order..." dialog first; give the API time.
- extendedWaitUntil:
visible: "Order Placed Successfully!"
timeout: 60000
- assertVisible: "Continue Shopping"

View file

@ -0,0 +1,14 @@
# Dismisses the React Native dev LogBox toast ("Open debugger to view warnings").
#
# In a dev build that toast sits over the bottom of the screen and swallows taps
# (it blocked the admin add-slot FAB and the user app's bottom tab bar). It only
# appears when the app logs a warning, so this is conditional — tapping its
# bottom-right X unconditionally could hit a real control instead.
appId: in.freshyo.adminui
---
- runFlow:
when:
visible: "Open debugger to view warnings."
commands:
- tapOn:
point: "91%,90%"

View file

@ -0,0 +1,66 @@
# From the order-success screen: back to home, capture the Next Order glimpse
# card, open the order from it and cancel it — so a run leaves no live orders
# behind. Shared by place-order.yaml and user-search-order.yaml.
appId: in.freshyo.app
---
- tapOn: "Continue Shopping"
# --- Home glimpse -----------------------------------------------------------
# NextOrderGlimpse sits below "Our Stores" in the home header, so scroll to it.
- scrollUntilVisible:
element:
text: "Upcoming Order"
direction: DOWN
centerElement: true
timeout: 30000
- assertVisible: "Track Order"
- takeScreenshot: "home-upcoming-order-glimpse"
# --- Open it from the glimpse and cancel ------------------------------------
# The card sets the navigation target and replaces to /me, which forwards to
# /(drawer)/(tabs)/me/my-orders/{id}.
- tapOn: "Upcoming Order"
- extendedWaitUntil:
visible: "Order Items"
timeout: 30000
- scrollUntilVisible:
element:
text: "Cancel Order"
direction: DOWN
centerElement: true
- tapOn: "Cancel Order"
- extendedWaitUntil:
visible: "Reason for cancellation"
timeout: 20000
- tapOn:
id: "cancel-reason"
- inputText: "Ordered by mistake"
- hideKeyboard
# The sheet slides back down as the keyboard collapses, and a tap during that
# animation misses. Tap the sheet's (non-interactive) warning text to force the
# layout to settle before confirming.
- tapOn: "Are you sure you want to cancel this order\\?.*"
- waitForAnimationToEnd:
timeout: 8000
# Tapping this button right after the sheet settles can be swallowed (the JS
# thread is still busy finishing the navigation), so keep tapping until the
# request actually goes out — while it is in flight the label is replaced by a
# spinner, which is what ends the loop.
- repeat:
times: 5
commands:
- runFlow:
when:
visible: "Confirm Cancellation"
commands:
- waitForAnimationToEnd:
timeout: 3000
- tapOn: "Confirm Cancellation"
# Native success alert, then the detail page refetches with the cancellation.
- extendedWaitUntil:
visible: "Order cancelled successfully"
timeout: 20000
- tapOn: "OK"
- extendedWaitUntil:
visible: "Cancellation Reason"
timeout: 30000

24
e2e/subflows/login.yaml Normal file
View file

@ -0,0 +1,24 @@
# Reusable login subflow.
#
# Runs only when the login screen is actually showing, so an already signed-in
# session is left untouched. (OTP login can't be automated — no dev bypass — so
# this uses the username + password path: "other ways to login".)
#
# Android-only suite (Android package id).
appId: in.freshyo.app
---
- runFlow:
when:
visible: "other ways to login"
commands:
- tapOn: "other ways to login"
- tapOn: "Enter your email or mobile"
- inputText: ${TEST_IDENTIFIER}
- tapOn: "Enter your password"
- inputText: ${TEST_PASSWORD}
- hideKeyboard
- tapOn: "Login"
# Wait for the login screen to go away (the caller decides where we land).
- extendedWaitUntil:
notVisible: "other ways to login"
timeout: 60000

View file

@ -8,6 +8,7 @@
"dev": "turbo run dev --parallel", "dev": "turbo run dev --parallel",
"lint": "turbo run lint", "lint": "turbo run lint",
"typecheck": "bash typecheck", "typecheck": "bash typecheck",
"e2e": "bash e2e/run.sh",
"web-ui": "bun run --filter web-ui", "web-ui": "bun run --filter web-ui",
"web-ui:dev": "bun run web-ui dev", "web-ui:dev": "bun run web-ui dev",
"web-ui:build": "bun run web-ui build", "web-ui:build": "bun run web-ui build",

View file

@ -29,6 +29,7 @@ export interface OrderCardActionProps<T> {
export interface LoginFormInputs { export interface LoginFormInputs {
mobile: string mobile: string
otp?: string otp?: string
identifier?: string
password?: string password?: string
} }