diff --git a/.commandcode/settings.json b/.commandcode/settings.json index a5ea5aa..1835bd3 100644 --- a/.commandcode/settings.json +++ b/.commandcode/settings.json @@ -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(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/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": [], "defaultMode": "default" diff --git a/.commandcode/taste/taste/taste.md b/.commandcode/taste/taste/taste.md index b261e42..1274807 100644 --- a/.commandcode/taste/taste/taste.md +++ b/.commandcode/taste/taste/taste.md @@ -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 - 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 +- 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 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 diff --git a/.gitignore b/.gitignore index 42935ab..199ddb7 100644 --- a/.gitignore +++ b/.gitignore @@ -132,4 +132,12 @@ dist .pnp.* -type-clusters/* \ No newline at end of file +type-clusters/* + +# ---> Maestro (root E2E suite) +/e2e/.maestro-output/ +/e2e/.env + +apps/user-ui/android/ +apps/admin-ui/android/ +.maestro-output/* \ No newline at end of file diff --git a/apps/admin-ui/app/(drawer)/dashboard/slots/slot-details.tsx b/apps/admin-ui/app/(drawer)/dashboard/slots/slot-details.tsx index 153f10f..9e762be 100644 --- a/apps/admin-ui/app/(drawer)/dashboard/slots/slot-details.tsx +++ b/apps/admin-ui/app/(drawer)/dashboard/slots/slot-details.tsx @@ -198,6 +198,7 @@ export default function SlotDetails() { {/* FAB for Edit Slot */} router.push(`/(drawer)/dashboard/slots/edit/${slot.id}` as any)} activeOpacity={0.95} style={{ position: 'absolute', bottom: 32, right: 24, zIndex: 100 }} diff --git a/apps/admin-ui/components/ProductsSelector.tsx b/apps/admin-ui/components/ProductsSelector.tsx index 2741a34..3da9ca7 100644 --- a/apps/admin-ui/components/ProductsSelector.tsx +++ b/apps/admin-ui/components/ProductsSelector.tsx @@ -28,6 +28,7 @@ interface ProductsSelectorProps { selectedGroupIds?: number[]; onGroupChange?: (groupIds: number[]) => void; showGroups?: boolean; + testID?: string; } export default function ProductsSelector({ @@ -44,6 +45,7 @@ export default function ProductsSelector({ showGroups = true, selectedGroupIds = [], onGroupChange, + testID, }: ProductsSelectorProps) { const { data: skusData } = trpc.common.product.getAllSkusSummary.useQuery(); const allSkus: SkuSummary[] = skusData?.skus || []; @@ -147,6 +149,7 @@ export default function ProductsSelector({ )} setFieldValue('selectedGroupIds', newGroupIds)} label="Select Products" + testID="slot-products-selector" placeholder="Select products for this slot" /> @@ -193,6 +194,7 @@ export default function SlotForm({ selectedGroupIds={snippet.groupIds || []} onGroupChange={(newGroupIds) => setFieldValue(`vendorSnippetList.${index}.groupIds`, newGroupIds)} label="Select Products" + testID="snippet-products-selector" placeholder="Select products for snippet" isDisabled={(sku) => !values.selectedSkuIds.includes(sku.id)} /> diff --git a/apps/backend/wrangler.dev.toml b/apps/backend/wrangler.dev.toml index bdcbe8b..1102f16 100644 --- a/apps/backend/wrangler.dev.toml +++ b/apps/backend/wrangler.dev.toml @@ -8,10 +8,10 @@ routes = [ [[d1_databases]] binding = "DB" -database_name = "freshyo-backend-dev" -database_id = "b2528bdb-0ec0-478e-8765-b118918153c0" -#database_name = "freshyo-dev" -#database_id = "3cad440f-dc14-4baa-ab05-04eb08e206de" +#database_name = "freshyo-backend-dev" +#database_id = "b2528bdb-0ec0-478e-8765-b118918153c0" +database_name = "freshyo-dev" +database_id = "3cad440f-dc14-4baa-ab05-04eb08e206de" migrations_dir="../../packages/db_helper_sqlite/drizzle" migrations_pattern="migration.sql" [durable_objects] diff --git a/apps/user-ui/app/(auth)/login.tsx b/apps/user-ui/app/(auth)/login.tsx index 972f64d..ad0c3c5 100644 --- a/apps/user-ui/app/(auth)/login.tsx +++ b/apps/user-ui/app/(auth)/login.tsx @@ -16,6 +16,7 @@ function Login() { const { loginWithToken } = useAuth(); const [isLoading, setIsLoading] = useState(false); const [step, setStep] = useState<'mobile' | 'choice' | 'otp' | 'password'>('mobile'); + const [passwordBackTo, setPasswordBackTo] = useState<'mobile' | 'choice' | 'otp'>('choice'); const [selectedMobile, setSelectedMobile] = useState(''); const [canResend, setCanResend] = useState(false); const [resendCountdown, setResendCountdown] = useState(0); @@ -135,8 +136,9 @@ function Login() { setError, clearErrors, setValue, + watch, } = useForm({ - defaultValues: { mobile: "", otp: "", password: "" }, + defaultValues: { mobile: "", otp: "", identifier: "", password: "" }, }); const validateMobile = (mobile: string): boolean => { @@ -223,7 +225,17 @@ function Login() { otp: data.otp, }); } 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) { setError("password", { type: "manual", @@ -234,7 +246,7 @@ function Login() { try { const response = await loginMutation.mutateAsync({ - identifier: selectedMobile, + identifier, password: data.password, }); loginWithToken(response.data.token, response.data.user); @@ -313,7 +325,11 @@ function Login() { setStep('password')} + onPress={() => { + setPasswordBackTo('choice'); + setValue('identifier', selectedMobile); + setStep('password'); + }} fillColor="gray1" textColor="black1" style={tw`flex-1 mr-2 border border-gray-200`} @@ -374,6 +390,8 @@ function Login() { { + setPasswordBackTo('choice'); + setValue('identifier', selectedMobile); setStep('password'); setValue('otp', ''); setOtpCells(['', '', '', '']); @@ -422,6 +440,32 @@ function Login() { {step === 'password' && ( <> + ( + + + + )} + /> + {errors.identifier && ( + + {errors.identifier.message} + + )} + { - setStep('choice'); + setStep(passwordBackTo); setValue('password', ''); clearErrors(); }} @@ -502,6 +546,22 @@ function Login() { : (step === 'otp' ? "Verify & Login" : step === 'password' ? "Login" : "Continue")} + + {step === 'mobile' && ( + { + setPasswordBackTo('mobile'); + setValue('identifier', watch('mobile') || ''); + setStep('password'); + clearErrors(); + }} + style={tw`mt-5`} + > + + other ways to login + + + )} )} diff --git a/apps/user-ui/app/(drawer)/(tabs)/me/my-orders/[id].tsx b/apps/user-ui/app/(drawer)/(tabs)/me/my-orders/[id].tsx index b1cfbd9..a1f27f6 100644 --- a/apps/user-ui/app/(drawer)/(tabs)/me/my-orders/[id].tsx +++ b/apps/user-ui/app/(drawer)/(tabs)/me/my-orders/[id].tsx @@ -403,6 +403,7 @@ export default function OrderDetails() { Reason for cancellation = ({ )} = ({ return ( = ({ ) : ( handleQuantityChange(1)} activeOpacity={0.8} diff --git a/apps/user-ui/components/cart-page.tsx b/apps/user-ui/components/cart-page.tsx index faa6d31..3d69b88 100644 --- a/apps/user-ui/components/cart-page.tsx +++ b/apps/user-ui/components/cart-page.tsx @@ -958,6 +958,7 @@ export default function CartPage({ isFlashDelivery = false }: FlashDeliveryProps Shop More { const availableItems = cartItems diff --git a/apps/user-ui/components/floating-cart-bar.tsx b/apps/user-ui/components/floating-cart-bar.tsx index f9c6c03..d9200c1 100644 --- a/apps/user-ui/components/floating-cart-bar.tsx +++ b/apps/user-ui/components/floating-cart-bar.tsx @@ -211,6 +211,7 @@ const FloatingCartBar: React.FC = ({ router.push( isFlashDelivery diff --git a/apps/user-ui/src/components/AddToCartDialog.tsx b/apps/user-ui/src/components/AddToCartDialog.tsx index a3acab8..b56e212 100644 --- a/apps/user-ui/src/components/AddToCartDialog.tsx +++ b/apps/user-ui/src/components/AddToCartDialog.tsx @@ -162,6 +162,7 @@ export default function AddToCartDialog() { {availableSlots.map((slot: any) => ( { @@ -241,6 +242,7 @@ export default function AddToCartDialog() { = ({ onSuccess, initialValues, isE {({ handleChange, handleBlur, handleSubmit, values, errors, touched, setFieldValue }) => ( = ({ onSuccess, initialValues, isE {touched.name && errors.name && {errors.name}} = ({ onSuccess, initialValues, isE {touched.phone && errors.phone && {errors.phone}} = ({ onSuccess, initialValues, isE handleSubmit()} disabled={isSubmitting} diff --git a/change-log.txt b/change-log.txt index c574da6..5ca474f 100644 --- a/change-log.txt +++ b/change-log.txt @@ -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). 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. + +[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: + { setPasswordBackTo('mobile'); setValue('identifier', watch('mobile') || ''); setStep('password'); clearErrors() }} style={tw`mt-5`}> + other ways to login + + +[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=`: + 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: + + 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//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: + + 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 /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, +... + /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`. diff --git a/e2e/config.yaml b/e2e/config.yaml new file mode 100644 index 0000000..94287b2 --- /dev/null +++ b/e2e/config.yaml @@ -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 diff --git a/e2e/flows/admin-attach-product.yaml b/e2e/flows/admin-attach-product.yaml new file mode 100644 index 0000000..419e06a --- /dev/null +++ b/e2e/flows/admin-attach-product.yaml @@ -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 #", +# 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 — " ". 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" diff --git a/e2e/flows/admin-product-full.yaml b/e2e/flows/admin-product-full.yaml new file mode 100644 index 0000000..d218851 --- /dev/null +++ b/e2e/flows/admin-product-full.yaml @@ -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" diff --git a/e2e/flows/admin-product.yaml b/e2e/flows/admin-product.yaml new file mode 100644 index 0000000..29f770a --- /dev/null +++ b/e2e/flows/admin-product.yaml @@ -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" diff --git a/e2e/flows/admin-slot.yaml b/e2e/flows/admin-slot.yaml new file mode 100644 index 0000000..a391c61 --- /dev/null +++ b/e2e/flows/admin-slot.yaml @@ -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 diff --git a/e2e/flows/place-order.yaml b/e2e/flows/place-order.yaml new file mode 100644 index 0000000..1ad229d --- /dev/null +++ b/e2e/flows/place-order.yaml @@ -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 diff --git a/e2e/flows/smoke.yaml b/e2e/flows/smoke.yaml new file mode 100644 index 0000000..164d5cc --- /dev/null +++ b/e2e/flows/smoke.yaml @@ -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 diff --git a/e2e/flows/user-search-order.yaml b/e2e/flows/user-search-order.yaml new file mode 100644 index 0000000..d996a72 --- /dev/null +++ b/e2e/flows/user-search-order.yaml @@ -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-/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 diff --git a/e2e/run.sh b/e2e/run.sh new file mode 100644 index 0000000..1165fcc --- /dev/null +++ b/e2e/run.sh @@ -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[@]}"} "$@" diff --git a/e2e/subflows/admin-login.yaml b/e2e/subflows/admin-login.yaml new file mode 100644 index 0000000..0de0d0f --- /dev/null +++ b/e2e/subflows/admin-login.yaml @@ -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 diff --git a/e2e/subflows/checkout-and-place-order.yaml b/e2e/subflows/checkout-and-place-order.yaml new file mode 100644 index 0000000..98b3ab9 --- /dev/null +++ b/e2e/subflows/checkout-and-place-order.yaml @@ -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" diff --git a/e2e/subflows/dismiss-dev-toast.yaml b/e2e/subflows/dismiss-dev-toast.yaml new file mode 100644 index 0000000..98ebed6 --- /dev/null +++ b/e2e/subflows/dismiss-dev-toast.yaml @@ -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%" diff --git a/e2e/subflows/glimpse-and-cancel.yaml b/e2e/subflows/glimpse-and-cancel.yaml new file mode 100644 index 0000000..4f84766 --- /dev/null +++ b/e2e/subflows/glimpse-and-cancel.yaml @@ -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 diff --git a/e2e/subflows/login.yaml b/e2e/subflows/login.yaml new file mode 100644 index 0000000..c6c9487 --- /dev/null +++ b/e2e/subflows/login.yaml @@ -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 diff --git a/package.json b/package.json index 64b4d6d..6074288 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dev": "turbo run dev --parallel", "lint": "turbo run lint", "typecheck": "bash typecheck", + "e2e": "bash e2e/run.sh", "web-ui": "bun run --filter web-ui", "web-ui:dev": "bun run web-ui dev", "web-ui:build": "bun run web-ui build", diff --git a/packages/shared/types/app-common.types.ts b/packages/shared/types/app-common.types.ts index 5d02d5c..f533fda 100644 --- a/packages/shared/types/app-common.types.ts +++ b/packages/shared/types/app-common.types.ts @@ -29,6 +29,7 @@ export interface OrderCardActionProps { export interface LoginFormInputs { mobile: string otp?: string + identifier?: string password?: string }