enh
This commit is contained in:
parent
d0b74fb61a
commit
b5274e0c2b
34 changed files with 1598 additions and 11 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -132,4 +132,12 @@ dist
|
|||
.pnp.*
|
||||
|
||||
|
||||
type-clusters/*
|
||||
type-clusters/*
|
||||
|
||||
# ---> Maestro (root E2E suite)
|
||||
/e2e/.maestro-output/
|
||||
/e2e/.env
|
||||
|
||||
apps/user-ui/android/
|
||||
apps/admin-ui/android/
|
||||
.maestro-output/*
|
||||
|
|
@ -198,6 +198,7 @@ export default function SlotDetails() {
|
|||
|
||||
{/* FAB for Edit Slot */}
|
||||
<MyTouchableOpacity
|
||||
testID="edit-slot-fab"
|
||||
onPress={() => router.push(`/(drawer)/dashboard/slots/edit/${slot.id}` as any)}
|
||||
activeOpacity={0.95}
|
||||
style={{ position: 'absolute', bottom: 32, right: 24, zIndex: 100 }}
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
)}
|
||||
|
||||
<BottomDropdown
|
||||
testID={testID}
|
||||
label={label}
|
||||
options={productOptions}
|
||||
value={multiple
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@ export default function SlotForm({
|
|||
selectedGroupIds={values.selectedGroupIds}
|
||||
onGroupChange={(newGroupIds) => setFieldValue('selectedGroupIds', newGroupIds)}
|
||||
label="Select Products"
|
||||
testID="slot-products-selector"
|
||||
placeholder="Select products for this slot"
|
||||
/>
|
||||
</View>
|
||||
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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<LoginFormInputs>({
|
||||
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() {
|
|||
<View style={tw`flex-row justify-between mb-6`}>
|
||||
<MyButton
|
||||
textContent="Use Password"
|
||||
onPress={() => 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() {
|
|||
|
||||
<MyTouchableOpacity
|
||||
onPress={() => {
|
||||
setPasswordBackTo('choice');
|
||||
setValue('identifier', selectedMobile);
|
||||
setStep('password');
|
||||
setValue('otp', '');
|
||||
setOtpCells(['', '', '', '']);
|
||||
|
|
@ -422,6 +440,32 @@ function Login() {
|
|||
|
||||
{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
|
||||
control={control}
|
||||
name="password"
|
||||
|
|
@ -450,7 +494,7 @@ function Login() {
|
|||
<View style={tw`flex-row justify-center mb-6`}>
|
||||
<MyTouchableOpacity
|
||||
onPress={() => {
|
||||
setStep('choice');
|
||||
setStep(passwordBackTo);
|
||||
setValue('password', '');
|
||||
clearErrors();
|
||||
}}
|
||||
|
|
@ -502,6 +546,22 @@ function Login() {
|
|||
: (step === 'otp' ? "Verify & Login" : step === 'password' ? "Login" : "Continue")}
|
||||
</MyButton>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@ export default function OrderDetails() {
|
|||
|
||||
<MyText style={tw`text-gray-700 font-medium mb-2`}>Reason for cancellation</MyText>
|
||||
<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}
|
||||
onChangeText={setCancelReason}
|
||||
|
|
|
|||
|
|
@ -337,6 +337,7 @@ const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
|
|||
</View>
|
||||
)}
|
||||
<MyTouchableOpacity
|
||||
testID="place-order-button"
|
||||
style={tw`bg-brand500 py-4 rounded-xl items-center shadow-md ${!selectedAddress ? 'opacity-50' : ''}`}
|
||||
disabled={!selectedAddress}
|
||||
onPress={handlePlaceOrder}
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
|
|||
return (
|
||||
<ContainerComp>
|
||||
<MyTouchableOpacity
|
||||
testID="product-card"
|
||||
style={[
|
||||
tw`bg-white overflow-hidden border border-gray-200`,
|
||||
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} />
|
||||
) : (
|
||||
<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`}
|
||||
onPress={() => handleQuantityChange(1)}
|
||||
activeOpacity={0.8}
|
||||
|
|
|
|||
|
|
@ -958,6 +958,7 @@ export default function CartPage({ isFlashDelivery = false }: FlashDeliveryProps
|
|||
<MyText style={tw`text-gray-700 font-bold`}>Shop More</MyText>
|
||||
</MyTouchableOpacity>
|
||||
<MyTouchableOpacity
|
||||
testID="checkout-button"
|
||||
style={tw`flex-1 bg-brand500 py-3.5 rounded-xl items-center shadow-md flex-row justify-center`}
|
||||
onPress={() => {
|
||||
const availableItems = cartItems
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
|
|||
</View>
|
||||
|
||||
<MyTouchableOpacity
|
||||
testID="go-to-cart"
|
||||
style={tw`bg-white px-3 py-2 rounded-2xl shadow-lg shadow-black/20`}
|
||||
onPress={() => router.push(
|
||||
isFlashDelivery
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ export default function AddToCartDialog() {
|
|||
{availableSlots.map((slot: any) => (
|
||||
<MyTouchableOpacity
|
||||
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'
|
||||
}`}
|
||||
onPress={() => {
|
||||
|
|
@ -241,6 +242,7 @@ export default function AddToCartDialog() {
|
|||
|
||||
<View style={tw`flex-row gap-3 mt-4 pb-12`}>
|
||||
<MyTouchableOpacity
|
||||
testID="add-to-cart-confirm"
|
||||
style={tw`flex-1 bg-brand500 py-3.5 rounded-xl items-center ${(!selectedSlotId && !selectedFlashDelivery) ? 'opacity-50' : ''}`}
|
||||
onPress={handleAddToCart}
|
||||
disabled={(addToCart.isLoading || updateCartItem.isLoading) || (!selectedSlotId && !selectedFlashDelivery)}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ const AddressForm: React.FC<AddressFormProps> = ({ onSuccess, initialValues, isE
|
|||
{({ handleChange, handleBlur, handleSubmit, values, errors, touched, setFieldValue }) => (
|
||||
<View style={tw`flex-col gap-2`}>
|
||||
<MyTextInput
|
||||
testID="address-name"
|
||||
placeholder="Name"
|
||||
shrunkPadding={true}
|
||||
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>}
|
||||
|
||||
<MyTextInput
|
||||
testID="address-phone"
|
||||
placeholder="Phone"
|
||||
shrunkPadding={true}
|
||||
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>}
|
||||
|
||||
<MyTextInput
|
||||
testID="address-line1"
|
||||
placeholder="Address Line 1"
|
||||
shrunkPadding={true}
|
||||
onChangeText={handleChange('addressLine1')}
|
||||
|
|
@ -227,6 +230,7 @@ const AddressForm: React.FC<AddressFormProps> = ({ onSuccess, initialValues, isE
|
|||
</View>
|
||||
|
||||
<MyTouchableOpacity
|
||||
testID="address-submit"
|
||||
style={tw`bg-indigo-600 p-3 rounded ${isSubmitting ? 'opacity-50' : ''}`}
|
||||
onPress={() => handleSubmit()}
|
||||
disabled={isSubmitting}
|
||||
|
|
|
|||
370
change-log.txt
370
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:
|
||||
<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
14
e2e/.env.example
Normal 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
125
e2e/README.md
Normal 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
21
e2e/config.yaml
Normal 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
|
||||
92
e2e/flows/admin-attach-product.yaml
Normal file
92
e2e/flows/admin-attach-product.yaml
Normal 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"
|
||||
243
e2e/flows/admin-product-full.yaml
Normal file
243
e2e/flows/admin-product-full.yaml
Normal 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"
|
||||
64
e2e/flows/admin-product.yaml
Normal file
64
e2e/flows/admin-product.yaml
Normal 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
103
e2e/flows/admin-slot.yaml
Normal 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
|
||||
65
e2e/flows/place-order.yaml
Normal file
65
e2e/flows/place-order.yaml
Normal 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
13
e2e/flows/smoke.yaml
Normal 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
|
||||
70
e2e/flows/user-search-order.yaml
Normal file
70
e2e/flows/user-search-order.yaml
Normal 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
77
e2e/run.sh
Normal 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[@]}"} "$@"
|
||||
25
e2e/subflows/admin-login.yaml
Normal file
25
e2e/subflows/admin-login.yaml
Normal 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
|
||||
96
e2e/subflows/checkout-and-place-order.yaml
Normal file
96
e2e/subflows/checkout-and-place-order.yaml
Normal 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"
|
||||
14
e2e/subflows/dismiss-dev-toast.yaml
Normal file
14
e2e/subflows/dismiss-dev-toast.yaml
Normal 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%"
|
||||
66
e2e/subflows/glimpse-and-cancel.yaml
Normal file
66
e2e/subflows/glimpse-and-cancel.yaml
Normal 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
24
e2e/subflows/login.yaml
Normal 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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export interface OrderCardActionProps<T> {
|
|||
export interface LoginFormInputs {
|
||||
mobile: string
|
||||
otp?: string
|
||||
identifier?: string
|
||||
password?: string
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue