enh
This commit is contained in:
parent
a40725213f
commit
192126cf9e
24 changed files with 385 additions and 166 deletions
|
|
@ -1,11 +1,19 @@
|
|||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import queryClient from "../utils/queryClient";
|
||||
import { Stack } from "expo-router";
|
||||
import { LogBox } from "react-native";
|
||||
import { StaffAuthProvider } from "@/components/context/staff-auth-context";
|
||||
import { trpc, trpcClient } from "@/src/trpc-client";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { RefreshProvider } from '../../../packages/ui/src/lib/refresh-context';
|
||||
|
||||
// Silence dev warnings/errors in LogBox. The "Open debugger to view warnings"
|
||||
// toast overlays the bottom of the UI and intercepts taps (e.g. the Add Slot
|
||||
// FAB), which breaks E2E flows.
|
||||
if (__DEV__) {
|
||||
LogBox.ignoreAllLogs(true);
|
||||
}
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
|
|
|||
|
|
@ -290,6 +290,7 @@ function Login() {
|
|||
render={({ field: { onChange, onBlur, value } }) => (
|
||||
<View style={tw`mb-5`}>
|
||||
<MyTextInput
|
||||
testID="user-login-mobile"
|
||||
topLabel="Mobile Number"
|
||||
placeholder="Enter your mobile number"
|
||||
value={value}
|
||||
|
|
@ -446,6 +447,7 @@ function Login() {
|
|||
render={({ field: { onChange, onBlur, value } }) => (
|
||||
<View style={tw`mb-4`}>
|
||||
<MyTextInput
|
||||
testID="user-login-identifier"
|
||||
topLabel="Username"
|
||||
placeholder="Enter your email or mobile"
|
||||
value={value}
|
||||
|
|
@ -473,6 +475,7 @@ function Login() {
|
|||
render={({ field: { onChange, onBlur, value } }) => (
|
||||
<View style={tw`mb-4`}>
|
||||
<MyTextInput
|
||||
testID="user-login-password"
|
||||
topLabel="Password"
|
||||
placeholder="Enter your password"
|
||||
value={value}
|
||||
|
|
@ -528,6 +531,7 @@ function Login() {
|
|||
|
||||
<View style={tw`mt-4`}>
|
||||
<MyButton
|
||||
testID="user-login-submit"
|
||||
onPress={handleSubmit(onSubmit)}
|
||||
fillColor="brand600"
|
||||
textColor="white1"
|
||||
|
|
@ -549,6 +553,7 @@ function Login() {
|
|||
|
||||
{step === 'mobile' && (
|
||||
<MyTouchableOpacity
|
||||
testID="user-login-other-ways"
|
||||
onPress={() => {
|
||||
setPasswordBackTo('mobile');
|
||||
setValue('identifier', watch('mobile') || '');
|
||||
|
|
|
|||
|
|
@ -696,6 +696,7 @@ export default function Dashboard() {
|
|||
style={searchBarContainerStyle}
|
||||
>
|
||||
<SearchBar
|
||||
testID="home-search"
|
||||
value={""}
|
||||
onChangeText={() => { }}
|
||||
onPress={handleSearchPress}
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ export default function SearchResults() {
|
|||
<View style={tw`pt-4 pb-2 px-4`}>
|
||||
<SearchBar
|
||||
ref={searchInputRef}
|
||||
testID="search-input"
|
||||
value={inputQuery}
|
||||
onChangeText={setInputQuery}
|
||||
onSubmitEditing={handleSearch}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { Stack } from "expo-router";
|
|||
import "react-native-reanimated";
|
||||
|
||||
import { useColorScheme } from "@/hooks/useColorScheme";
|
||||
import { Appearance, StatusBar, View } from "react-native";
|
||||
import { Appearance, StatusBar, View, LogBox } from "react-native";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { theme , MyStatusBar } from "common-ui";
|
||||
import queryClient from "@/utils/queryClient";
|
||||
|
|
@ -31,6 +31,13 @@ import React from "react";
|
|||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import NotifChecker from "@/services/notif-service/notif-checker";
|
||||
|
||||
// Silence dev warnings/errors in LogBox. The "Open debugger to view warnings"
|
||||
// toast overlays the bottom of the UI and intercepts taps (e.g. tab bar /
|
||||
// bottom-sheet buttons), which breaks E2E flows.
|
||||
if (__DEV__) {
|
||||
LogBox.ignoreAllLogs(true);
|
||||
}
|
||||
|
||||
export default function RootLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
const [loaded] = useFonts({
|
||||
|
|
|
|||
|
|
@ -160,9 +160,10 @@ const CheckoutAddressSelector: React.FC<AddressSelectorProps> = ({
|
|||
decelerationRate="fast"
|
||||
snapToInterval={CARD_WIDTH}
|
||||
>
|
||||
{sortedAddresses.map((address) => (
|
||||
{sortedAddresses.map((address, index) => (
|
||||
<TouchableOpacity
|
||||
key={address.id}
|
||||
testID={`address-option-${index}`}
|
||||
onPress={() => {
|
||||
onAddressSelect(address.id);
|
||||
resetScrollToLeft();
|
||||
|
|
|
|||
|
|
@ -328,8 +328,8 @@ const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
|
|||
|
||||
{/* Bottom Action Bar */}
|
||||
<View style={tw`bg-white p-5 rounded-2xl shadow-sm mb-4 border border-gray-100`}>
|
||||
{!selectedAddress && (
|
||||
<View style={tw`bg-red-50 p-3 rounded-lg mb-3 flex-row items-center`}>
|
||||
{!selectedAddress && (
|
||||
<View testID="select-address-error" style={tw`bg-red-50 p-3 rounded-lg mb-3 flex-row items-center`}>
|
||||
<MaterialIcons name="error-outline" size={16} color="#EF4444" style={tw`mr-2`} />
|
||||
<MyText style={tw`text-red-600 text-sm font-medium`}>
|
||||
Please select a delivery address to place order
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ export default function AddToCartDialog() {
|
|||
{availableSlots.map((slot: any) => (
|
||||
<MyTouchableOpacity
|
||||
key={slot.id}
|
||||
testID="slot-option"
|
||||
testID={`slot-${dayjs(slot.deliveryTime).format('YYYY-MM-DD-HH-mm')}`}
|
||||
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={() => {
|
||||
|
|
|
|||
149
change-log.txt
149
change-log.txt
|
|
@ -2382,6 +2382,155 @@ again; callers invoke it only when the login screen is showing.
|
|||
commands:
|
||||
- runFlow: <login path>
|
||||
|
||||
[2026-09-16 19:41:02] Disable LogBox in dev builds (admin-ui + user-ui).
|
||||
|
||||
The dev LogBox toast ("Open debugger to view warnings.") overlays the bottom of
|
||||
the UI and intercepts taps (e.g. the Add Slot FAB), which breaks E2E flows.
|
||||
|
||||
=== apps/admin-ui/app/_layout.tsx ===
|
||||
- import: + LogBox from 'react-native'
|
||||
- module top: `if (__DEV__) { LogBox.ignoreAllLogs(true) }`
|
||||
|
||||
=== apps/user-ui/app/_layout.tsx ===
|
||||
- import: + LogBox from 'react-native'
|
||||
- module top: `if (__DEV__) { LogBox.ignoreAllLogs(true) }`
|
||||
|
||||
[2026-09-16 19:46:50] Add a standalone image-upload dev flow + image remove testID.
|
||||
|
||||
Standalone flow to iterate on the image picker without running the whole product flow.
|
||||
|
||||
=== packages/ui/src/components/ImageUploaderNeo.tsx ===
|
||||
- per-image remove button: + testID={testID ? `${testID}-remove` : undefined}
|
||||
|
||||
=== tests/e2e/dev/upload-image.yaml (NEW) ===
|
||||
- addMedia -> launchApp -> conditional login -> add-product-menu-item ->
|
||||
scroll to variant-0-image-add -> tap -> runFlow subflows/pick-image.yaml ->
|
||||
assert variant-0-image-add-remove visible
|
||||
- lives in dev/ so the root suite glob does not pick it up
|
||||
|
||||
Run with: bash tests/e2e/run.sh dev/upload-image.yaml
|
||||
|
||||
[2026-09-16 19:50:12] Speed up the image-picker subflow.
|
||||
|
||||
`pick-image.yaml` was a chain of ~7 `optional: true` taps; each miss waited the
|
||||
full element timeout, so a single image took ~35-45s.
|
||||
|
||||
=== tests/e2e/subflows/pick-image.yaml ===
|
||||
- now: extendedWaitUntil for the thumbnail, tap it, extendedWaitUntil for
|
||||
button_add, tap it, then assert app-root
|
||||
- ids use a regex matching either MediaProvider package:
|
||||
`com\.(google\.)?android\.providers\.media\.module:id/(icon_thumbnail|button_add)`
|
||||
- dropped the DocumentsUI / permission-dialog optional fallbacks and the final
|
||||
guarded fallback block (no longer needed; keeps it fast)
|
||||
- if the picker doesn't open, it now fails at the first wait instead of timing
|
||||
out ~7 times
|
||||
|
||||
[2026-09-16 19:53:11] Remove the temporary image-upload dev flow.
|
||||
|
||||
=== tests/e2e/dev/upload-image.yaml ===
|
||||
- deleted (served its purpose; picker flow is now fast and verified)
|
||||
|
||||
[2026-09-16 20:04:01] Re-add the user-app order flow (rebuilt).
|
||||
|
||||
Runs after add-product + add-slot-with-product: opens the user app, logs in,
|
||||
search the created product, adds it to the cart in the created slot, checks out
|
||||
and places a COD order.
|
||||
|
||||
=== tests/e2e/user-order.yaml (NEW) ===
|
||||
- launchApp(in.freshyo.app) clearState -> optional FirstUser "Next"
|
||||
- openLink freshyo://login (x2) -> wait user-login-mobile -> conditional
|
||||
runFlow subflows/login-user.yaml
|
||||
- wait home-search -> tap home-search -> search-input-input -> inputText
|
||||
${PRODUCT_NAME} -> pressKey enter -> tap "Add to Cart"
|
||||
- wait slot-option -> tap slot-option -> tap add-to-cart-confirm
|
||||
- openLink freshyo://home/cart -> scrollUntilVisible checkout-button -> tap
|
||||
- extendedWaitUntil notVisible "Please select a delivery address" (address
|
||||
auto-select) -> scrollUntilVisible place-order-button -> tap
|
||||
- assertVisible "Order Placed Successfully!"
|
||||
|
||||
=== tests/e2e/subflows/login-user.yaml (NEW) ===
|
||||
- mobile -> "other ways to login" -> password -> Login
|
||||
|
||||
=== tests/e2e/config.yaml ===
|
||||
- executionOrder.flowsOrder += user-order
|
||||
|
||||
=== tests/e2e/run.sh ===
|
||||
- + -e USER_APP_ID / USER_MOBILE / USER_PASSWORD
|
||||
|
||||
[2026-09-16 20:09:14] user-order.yaml: login only if checkout demands it.
|
||||
|
||||
No more `openLink freshyo://login`: launchApp (clearState) -> optional FirstUser
|
||||
"Next" -> home-search -> search product -> "Add to Cart" -> slot-option ->
|
||||
confirm -> deep link freshyo://home/cart -> checkout-button. Then wait for
|
||||
`user-login-mobile|place-order-button`; run subflows/login-user.yaml only when
|
||||
the login screen is showing (auth gate). Address auto-select wait and Place
|
||||
Order unchanged.
|
||||
|
||||
[2026-09-16 20:15:02] user-order.yaml: post-login lands on cart, re-enter checkout.
|
||||
|
||||
Login returns to the cart page (not checkout), so after the conditional
|
||||
subflows/login-user.yaml there is now a second conditional runFlow: when
|
||||
`checkout-button` is visible (cart page), scroll to it and tap it to return to
|
||||
checkout. No-op when already on checkout. YAML validates (2 docs).
|
||||
|
||||
[2026-09-16 20:21:33] user-order.yaml: fix post-login recovery (button below fold).
|
||||
|
||||
The separate `when: visible: checkout-button` conditional never fired because
|
||||
Maestro visibility needs the element on-screen. Folded the recovery into the
|
||||
login branch instead: after subflows/login-user.yaml, wait for
|
||||
`${PRODUCT_NAME}` (cart loaded), then scrollUntilVisible checkout-button DOWN
|
||||
and tap it. No visibility gate on the button itself. YAML validates (2 docs).
|
||||
|
||||
[2026-09-16 20:28:44] user-order: tap first address card when none auto-selected.
|
||||
|
||||
Checkout auto-select only picks the address flagged isDefault
|
||||
(CheckoutAddressSelector.tsx:65-72); with none marked default the "Please
|
||||
select a delivery address" banner persisted and Place Order stayed disabled.
|
||||
Added testID `address-option-{index}` to the address cards and a conditional
|
||||
step in user-order.yaml: when the banner is visible, wait for address-option-0
|
||||
and tap it, then continue to Place Order.
|
||||
|
||||
[2026-09-16 20:35:10] user-order: read the address error at the bottom first.
|
||||
|
||||
The "select address" banner sits at the bottom next to Place Order, so a
|
||||
`when: visible:` check from the top of the page never fires. Reordered:
|
||||
wait for address-option-0 (cards loaded at top) -> scroll DOWN to
|
||||
place-order-button -> only then check the banner; if visible, scroll back UP
|
||||
to address-option-0, tap it, and scroll back DOWN before Place Order.
|
||||
|
||||
[2026-09-16 20:41:02] user-order: testID for the select-address error banner.
|
||||
|
||||
Added testID `select-address-error` to the "Please select a delivery address"
|
||||
banner in PaymentAndOrderComponent.tsx; user-order.yaml now matches on that id
|
||||
instead of text for both the `when: visible:` check and the `notVisible` wait.
|
||||
|
||||
[2026-09-16 20:56:12] E2E: deterministic slot via SLOT_DATE/HOUR/MIN + go-to-cart nav.
|
||||
|
||||
.env (+ .env.example) gains SLOT_DATE (today, YYYY-MM-DD), SLOT_HOUR, SLOT_MIN
|
||||
(delivery wall-time, 24h zero-padded, device-local); run.sh requires them,
|
||||
forwards them, and computes SLOT_FREEZE_HOUR/MIN = delivery - 10 min in bash.
|
||||
set-slot-times.yaml rewritten to absolute times (date pickers just confirm
|
||||
today; time pickers use text-input mode with env values; no evalScript).
|
||||
AddToCartDialog slot options get testID `slot-YYYY-MM-DD-HH-mm` (replaces
|
||||
generic `slot-option`). user-order.yaml taps `slot-${SLOT_DATE}-${SLOT_HOUR}-
|
||||
${SLOT_MIN}` and reaches the cart via the floating `go-to-cart` bar instead of
|
||||
the `freshyo://home/cart` deep link. README layout lines updated.
|
||||
|
||||
[2026-09-16 21:02:33] user-order: normalize post-login landing via relaunch.
|
||||
|
||||
Login may land on the cart page or the home page, so detecting the landing is
|
||||
fragile. After subflows/login-user.yaml the flow now relaunches the app
|
||||
without clearState (session + cart persist; initial route is always home),
|
||||
then goes home -> go-to-cart -> cart -> checkout-button like the main path.
|
||||
|
||||
[2026-09-16 21:06:12] user-order: handle home landing inline, no relaunch.
|
||||
|
||||
Reverted the post-login relaunch. After subflows/login-user.yaml the flow now
|
||||
assumes the home landing: wait for home-search, tap the floating go-to-cart
|
||||
bar, then scroll to checkout-button and re-enter checkout.
|
||||
|
||||
[2026-09-16 21:12:40] E2E: stripped narration comments from flows, run.sh, .env.
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
4
opencode.jsonc
Normal file
4
opencode.jsonc
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// {
|
||||
// "$schema": "https://opencode.ai/config.json",
|
||||
// "permission": "allow"
|
||||
// }
|
||||
|
|
@ -48,6 +48,7 @@ const ImageUploaderNeo: React.FC<ImageUploaderNeoProps & { testID?: string }> =
|
|||
style={tw`w-full aspect-square rounded`}
|
||||
/>
|
||||
<MyTouchableOpacity
|
||||
testID={testID ? `${testID}-remove` : undefined}
|
||||
onPress={() =>
|
||||
onImageRemove({
|
||||
url: image.imgUrl,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ interface SearchBarProps {
|
|||
containerStyle?: ViewStyle;
|
||||
onPress?: () => void;
|
||||
activeOpacity?: number;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const SearchBar = forwardRef<any, SearchBarProps>(({
|
||||
|
|
@ -28,6 +29,7 @@ const SearchBar = forwardRef<any, SearchBarProps>(({
|
|||
containerStyle,
|
||||
onPress,
|
||||
activeOpacity = 0.8,
|
||||
testID,
|
||||
}, ref) => {
|
||||
const Wrapper = onPress ? MyTouchableOpacity : View;
|
||||
|
||||
|
|
@ -35,8 +37,10 @@ const SearchBar = forwardRef<any, SearchBarProps>(({
|
|||
style: [tw`flex-row items-center bg-white rounded-2xl px-4 h-12 mb-6 shadow-md`, { overflow: 'visible' }, containerStyle],
|
||||
onPress,
|
||||
activeOpacity,
|
||||
testID,
|
||||
} : {
|
||||
style: [tw`flex-row items-center bg-white rounded-2xl px-4 h-12 mb-6 shadow-md`, { overflow: 'visible' }, containerStyle],
|
||||
testID,
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -44,6 +48,7 @@ const SearchBar = forwardRef<any, SearchBarProps>(({
|
|||
<MaterialIcons name="search" size={24} color={theme.colors.brand500} />
|
||||
<MyTextInput
|
||||
ref={ref}
|
||||
testID={testID ? `${testID}-input` : undefined}
|
||||
style={[tw`flex-1 text-base text-gray-800 bg-transparent h-8`, { borderWidth: 0 }]}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
|
|
|
|||
|
|
@ -1,29 +1,15 @@
|
|||
# Copy this file to `tests/e2e/.env` and fill in real values.
|
||||
# `tests/e2e/run.sh` loads it. `.env` is gitignored.
|
||||
# Quote values that contain spaces (they are fine either way with run.sh).
|
||||
|
||||
# App under test
|
||||
APP_ID=in.freshyo.adminui
|
||||
|
||||
# Admin login (must exist on the target backend)
|
||||
STAFF_NAME=
|
||||
STAFF_PASSWORD=
|
||||
|
||||
# Product under test. Used verbatim: add-product.yaml creates it with this exact
|
||||
# name and add-slot-with-product.yaml searches for it. The backend rejects
|
||||
# duplicate names, so change it (or delete the product) between suite runs.
|
||||
PRODUCT_NAME="E2E Test Product"
|
||||
|
||||
# Id of the store to pick in the Store dropdown (see the Stores screen / DB)
|
||||
STORE_ID=1
|
||||
|
||||
# Variant 1 (regular SKU)
|
||||
SKU1_QTY="500 g"
|
||||
SKU1_PRICE=199
|
||||
SKU1_MRP=249
|
||||
|
||||
# Variant 2 (flash SKU)
|
||||
SKU2_QTY="1 kg"
|
||||
SKU2_PRICE=349
|
||||
SKU2_MRP=399
|
||||
SKU2_FLASH_PRICE=299
|
||||
SLOT_DATE=
|
||||
SLOT_HOUR=
|
||||
SLOT_MIN=
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ Maestro flows that drive the **admin-ui** Expo app on an Android emulator.
|
|||
tests/e2e/
|
||||
config.yaml # workspace config (entry flows + execution order)
|
||||
add-product.yaml # create a product with 2 SKUs (one flash)
|
||||
add-slot-with-product.yaml # create a slot (delivery +60m, freeze +50m) holding that product
|
||||
add-slot-with-product.yaml # create a slot (SLOT_DATE + SLOT_HOUR:SLOT_MIN) holding that product
|
||||
subflows/
|
||||
login.yaml # reusable login (callers invoke it conditionally)
|
||||
pick-image.yaml # OS photo-picker adapter (Android)
|
||||
set-slot-times.yaml # delivery +60m / freeze +50m via native pickers
|
||||
set-slot-times.yaml # absolute delivery time + freeze (-10m) via native pickers
|
||||
dev/
|
||||
add-empty-slot.yaml # standalone: create a slot with no products (validate pickers)
|
||||
assets/test-product.png # seeded into the emulator gallery via `addMedia`
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
appId: ${APP_ID || "in.freshyo.adminui"}
|
||||
env:
|
||||
# Values are supplied via `-e` by run.sh. The inline `||` defaults let the
|
||||
# flow run standalone against a dev build without run.sh.
|
||||
APP_ID: ${APP_ID || "in.freshyo.adminui"}
|
||||
STAFF_NAME: ${STAFF_NAME || ""}
|
||||
STAFF_PASSWORD: ${STAFF_PASSWORD || ""}
|
||||
|
|
@ -18,17 +16,18 @@ tags:
|
|||
- products
|
||||
- smoke
|
||||
---
|
||||
# Seed the emulator gallery so the OS picker has an image to select.
|
||||
- addMedia:
|
||||
- './assets/test-product.png'
|
||||
|
||||
# Fresh state (logs out) + grant gallery access.
|
||||
- launchApp:
|
||||
clearState: true
|
||||
permissions:
|
||||
storage: allow
|
||||
|
||||
# --- Login (only when the login screen is showing) ---
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: login-name-input
|
||||
timeout: 180000
|
||||
- runFlow:
|
||||
when:
|
||||
visible:
|
||||
|
|
@ -36,13 +35,11 @@ tags:
|
|||
commands:
|
||||
- runFlow: subflows/login.yaml
|
||||
|
||||
# --- Navigate to Add Product ---
|
||||
- tapOn:
|
||||
id: add-product-menu-item
|
||||
- assertVisible:
|
||||
id: product-name-input
|
||||
|
||||
# --- Product level ---
|
||||
- tapOn:
|
||||
id: product-name-input
|
||||
- inputText: ${PRODUCT_NAME}
|
||||
|
|
@ -66,7 +63,6 @@ tags:
|
|||
- tapOn:
|
||||
id: product-type-dropdown-option-item
|
||||
|
||||
# --- Variant 1 (regular) ---
|
||||
- scrollUntilVisible:
|
||||
element:
|
||||
id: variant-0-attr-0-value
|
||||
|
|
@ -102,7 +98,6 @@ tags:
|
|||
id: variant-0-image-add
|
||||
- runFlow: subflows/pick-image.yaml
|
||||
|
||||
# --- Add variant 2 (flash) ---
|
||||
- scrollUntilVisible:
|
||||
element:
|
||||
id: add-variant-button
|
||||
|
|
@ -137,7 +132,6 @@ tags:
|
|||
- inputText: ${SKU2_MRP}
|
||||
- hideKeyboard
|
||||
|
||||
# Mark variant 2 as flash and set its flash price.
|
||||
- scrollUntilVisible:
|
||||
element:
|
||||
id: variant-1-flash
|
||||
|
|
@ -162,7 +156,6 @@ tags:
|
|||
id: variant-1-image-add
|
||||
- runFlow: subflows/pick-image.yaml
|
||||
|
||||
# --- Submit ---
|
||||
- scrollUntilVisible:
|
||||
element:
|
||||
id: create-product-button
|
||||
|
|
|
|||
|
|
@ -3,16 +3,19 @@ env:
|
|||
APP_ID: ${APP_ID || "in.freshyo.adminui"}
|
||||
STAFF_NAME: ${STAFF_NAME || ""}
|
||||
STAFF_PASSWORD: ${STAFF_PASSWORD || ""}
|
||||
# Must be the exact name created by add-product.yaml.
|
||||
PRODUCT_NAME: ${PRODUCT_NAME || "E2E Test Product"}
|
||||
SLOT_DATE: ${SLOT_DATE || ""}
|
||||
SLOT_HOUR: ${SLOT_HOUR || ""}
|
||||
SLOT_MIN: ${SLOT_MIN || ""}
|
||||
tags:
|
||||
- slots
|
||||
- smoke
|
||||
---
|
||||
# Restart the app so it returns to its initial route (dashboard when already
|
||||
# logged in from add-product.yaml, login otherwise).
|
||||
- launchApp
|
||||
# Login only when the login screen is showing (already authenticated in-suite).
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: 'login-name-input|add-product-menu-item'
|
||||
timeout: 180000
|
||||
- runFlow:
|
||||
when:
|
||||
visible:
|
||||
|
|
@ -20,24 +23,14 @@ tags:
|
|||
commands:
|
||||
- runFlow: subflows/login.yaml
|
||||
|
||||
# --- Open the Slots list from the dashboard, then the create-slot screen ---
|
||||
- scrollUntilVisible:
|
||||
element:
|
||||
id: delivery-slots-menu-item
|
||||
direction: DOWN
|
||||
- tapOn:
|
||||
id: delivery-slots-menu-item
|
||||
- assertVisible:
|
||||
id: add-slot-fab
|
||||
- tapOn:
|
||||
id: add-slot-fab
|
||||
- assertVisible:
|
||||
id: delivery-date-picker
|
||||
- openLink: freshyoadmin://dashboard/slots/add
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: delivery-date-picker
|
||||
timeout: 30000
|
||||
|
||||
# --- Delivery (+60m) and Freeze (+50m) date/times ---
|
||||
- runFlow: subflows/set-slot-times.yaml
|
||||
|
||||
# --- Add the product to the slot (search the exact name, select its SKUs) ---
|
||||
- scrollUntilVisible:
|
||||
element:
|
||||
id: slot-products-selector
|
||||
|
|
@ -57,7 +50,6 @@ tags:
|
|||
- tapOn:
|
||||
id: slot-products-selector-done
|
||||
|
||||
# --- Submit ---
|
||||
- scrollUntilVisible:
|
||||
element:
|
||||
id: create-slot-button
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
# Maestro workspace config for the root E2E suite.
|
||||
# Only YAML files in this folder are entry flows; `subflows/` is invoked
|
||||
# explicitly via runFlow and is intentionally not matched here.
|
||||
flows:
|
||||
- '*.yaml'
|
||||
- '!config.yaml'
|
||||
|
||||
# add-product must run first so add-slot-with-product can find the product.
|
||||
executionOrder:
|
||||
continueOnFailure: false
|
||||
flowsOrder:
|
||||
- add-product
|
||||
- add-slot-with-product
|
||||
- user-order
|
||||
|
|
|
|||
|
|
@ -3,16 +3,17 @@ env:
|
|||
APP_ID: ${APP_ID || "in.freshyo.adminui"}
|
||||
STAFF_NAME: ${STAFF_NAME || ""}
|
||||
STAFF_PASSWORD: ${STAFF_PASSWORD || ""}
|
||||
SLOT_DATE: ${SLOT_DATE || ""}
|
||||
SLOT_HOUR: ${SLOT_HOUR || ""}
|
||||
SLOT_MIN: ${SLOT_MIN || ""}
|
||||
tags:
|
||||
- dev
|
||||
---
|
||||
# Standalone flow: creates a slot (delivery +60m, freeze +50m) with NO products.
|
||||
# Handy for validating the native date/time pickers in isolation.
|
||||
#
|
||||
# Run with:
|
||||
# bash tests/e2e/run.sh dev/add-empty-slot.yaml
|
||||
- launchApp
|
||||
# Login only when the login screen is showing.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: 'login-name-input|add-product-menu-item'
|
||||
timeout: 180000
|
||||
- runFlow:
|
||||
when:
|
||||
visible:
|
||||
|
|
@ -20,7 +21,6 @@ tags:
|
|||
commands:
|
||||
- runFlow: ../subflows/login.yaml
|
||||
|
||||
# Open the Slots list from the dashboard, then the create-slot screen.
|
||||
- scrollUntilVisible:
|
||||
element:
|
||||
id: delivery-slots-menu-item
|
||||
|
|
|
|||
|
|
@ -1,13 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Runs the Maestro E2E suite against a running Android emulator.
|
||||
#
|
||||
# Values come from tests/e2e/.env (copy .env.example first). Maestro's CLI does
|
||||
# not load .env files itself, so we read it here and forward each value with `-e`.
|
||||
#
|
||||
# Usage:
|
||||
# bash tests/e2e/run.sh # run every entry flow in tests/e2e/
|
||||
# bash tests/e2e/run.sh add-product.yaml
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
|
|
@ -16,10 +7,8 @@ if [ ! -f .env ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# Load .env line-by-line instead of `source`-ing it, so values that contain
|
||||
# spaces (e.g. `SKU1_QTY=500 g`) work without needing quotes.
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
line="${line%$'\r'}" # tolerate CRLF
|
||||
line="${line%$'\r'}"
|
||||
case "$line" in
|
||||
'' | '#'*) continue ;;
|
||||
esac
|
||||
|
|
@ -31,16 +20,21 @@ done < ./.env
|
|||
: "${STAFF_PASSWORD:?STAFF_PASSWORD is required}"
|
||||
: "${PRODUCT_NAME:?PRODUCT_NAME is required}"
|
||||
: "${STORE_ID:?STORE_ID is required}"
|
||||
: "${SLOT_DATE:?SLOT_DATE is required (YYYY-MM-DD, must be today)}"
|
||||
: "${SLOT_HOUR:?SLOT_HOUR is required (delivery hour, 24h zero-padded)}"
|
||||
: "${SLOT_MIN:?SLOT_MIN is required (delivery minute, zero-padded)}"
|
||||
|
||||
# PRODUCT_NAME is used verbatim (the slot flow searches for the exact name).
|
||||
# The backend rejects duplicate product names, so change it between suite runs.
|
||||
export PRODUCT_NAME
|
||||
|
||||
# Default to the whole suite when no path/tags are passed.
|
||||
if [ "$#" -eq 0 ]; then
|
||||
set -- .
|
||||
fi
|
||||
|
||||
freeze_total=$((10#$SLOT_HOUR * 60 + 10#$SLOT_MIN - 10))
|
||||
SLOT_FREEZE_HOUR=$(printf '%02d' $((freeze_total / 60)))
|
||||
SLOT_FREEZE_MIN=$(printf '%02d' $((freeze_total % 60)))
|
||||
export SLOT_FREEZE_HOUR SLOT_FREEZE_MIN
|
||||
|
||||
maestro test \
|
||||
-e APP_ID="$APP_ID" \
|
||||
-e STAFF_NAME="$STAFF_NAME" \
|
||||
|
|
@ -54,4 +48,12 @@ maestro test \
|
|||
-e SKU2_PRICE="${SKU2_PRICE:-}" \
|
||||
-e SKU2_MRP="${SKU2_MRP:-}" \
|
||||
-e SKU2_FLASH_PRICE="${SKU2_FLASH_PRICE:-}" \
|
||||
-e SLOT_DATE="$SLOT_DATE" \
|
||||
-e SLOT_HOUR="$SLOT_HOUR" \
|
||||
-e SLOT_MIN="$SLOT_MIN" \
|
||||
-e SLOT_FREEZE_HOUR="$SLOT_FREEZE_HOUR" \
|
||||
-e SLOT_FREEZE_MIN="$SLOT_FREEZE_MIN" \
|
||||
-e USER_APP_ID="${USER_APP_ID:-in.freshyo.app}" \
|
||||
-e USER_MOBILE="${USER_MOBILE:-}" \
|
||||
-e USER_PASSWORD="${USER_PASSWORD:-}" \
|
||||
"$@"
|
||||
|
|
|
|||
17
tests/e2e/subflows/login-user.yaml
Normal file
17
tests/e2e/subflows/login-user.yaml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
appId: ${USER_APP_ID}
|
||||
env:
|
||||
USER_MOBILE: ${USER_MOBILE || ""}
|
||||
USER_PASSWORD: ${USER_PASSWORD || ""}
|
||||
---
|
||||
- tapOn:
|
||||
id: user-login-mobile
|
||||
- inputText: ${USER_MOBILE}
|
||||
- pressKey: enter
|
||||
- tapOn:
|
||||
id: user-login-other-ways
|
||||
- tapOn:
|
||||
id: user-login-password
|
||||
- inputText: ${USER_PASSWORD}
|
||||
- pressKey: enter
|
||||
- tapOn:
|
||||
id: user-login-submit
|
||||
|
|
@ -3,10 +3,6 @@ env:
|
|||
STAFF_NAME: ${STAFF_NAME || ""}
|
||||
STAFF_PASSWORD: ${STAFF_PASSWORD || ""}
|
||||
---
|
||||
# Logs into the admin app. Callers invoke this conditionally, only when the
|
||||
# login screen is showing (see the `when: visible id: login-name-input` wrapper
|
||||
# used by the entry flows), so it can also be run standalone from a logged-out
|
||||
# state.
|
||||
- tapOn:
|
||||
id: login-name-input
|
||||
- inputText: ${STAFF_NAME}
|
||||
|
|
@ -16,6 +12,5 @@ env:
|
|||
- hideKeyboard
|
||||
- tapOn:
|
||||
id: login-button
|
||||
# Dashboard quick actions render once the drawer shell is up.
|
||||
- assertVisible:
|
||||
id: add-product-menu-item
|
||||
|
|
|
|||
|
|
@ -1,78 +1,25 @@
|
|||
appId: ${APP_ID || "in.freshyo.adminui"}
|
||||
---
|
||||
# Android system picker adapter.
|
||||
#
|
||||
# The picker is OS-level UI in a separate window, so it cannot be targeted with
|
||||
# app testIDs. Selectors here are system ids/text, and every step is `optional`
|
||||
# so different OS versions fall through until one matches.
|
||||
#
|
||||
# Confirmed from AOSP MediaProvider (activity_photo_picker.xml):
|
||||
# thumbnail : @id/icon_thumbnail
|
||||
# confirm : @id/button_add (Button, text "Add", bottom-right)
|
||||
# The provider package is `com.google.android.providers.media.module` on Google
|
||||
# Play images and `com.android.providers.media.module` on AOSP images.
|
||||
- runFlow:
|
||||
when:
|
||||
platform: Android
|
||||
commands:
|
||||
# Runtime permission prompt (older Android / first launch).
|
||||
- tapOn:
|
||||
text: 'Allow'
|
||||
optional: true
|
||||
|
||||
# Select the first thumbnail: Android 13/14 Photo Picker, then the older
|
||||
# DocumentsUI gallery.
|
||||
- tapOn:
|
||||
id: 'com.google.android.providers.media.module:id/icon_thumbnail'
|
||||
index: 0
|
||||
optional: true
|
||||
- tapOn:
|
||||
id: 'com.android.providers.media.module:id/icon_thumbnail'
|
||||
index: 0
|
||||
optional: true
|
||||
- tapOn:
|
||||
id: 'com.google.android.documentsui:id/thumbnail'
|
||||
index: 0
|
||||
optional: true
|
||||
- tapOn:
|
||||
id: 'com.android.documentsui:id/thumbnail'
|
||||
index: 0
|
||||
optional: true
|
||||
|
||||
- waitForAnimationToEnd
|
||||
|
||||
# Confirm the selection by its exact id.
|
||||
- tapOn:
|
||||
id: 'com.google.android.providers.media.module:id/button_add'
|
||||
optional: true
|
||||
- tapOn:
|
||||
id: 'com.android.providers.media.module:id/button_add'
|
||||
optional: true
|
||||
- tapOn:
|
||||
id: 'com.google.android.documentsui:id/button_add'
|
||||
optional: true
|
||||
|
||||
# Give the picker a moment to close.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: app-root
|
||||
timeout: 3000
|
||||
optional: true
|
||||
id: 'com\.(google\.)?android\.providers\.media\.module:id/icon_thumbnail'
|
||||
timeout: 15000
|
||||
|
||||
# Fallbacks run ONLY while the picker is still open (app root not visible),
|
||||
# so they can never tap in-app controls like "Add Variant".
|
||||
- runFlow:
|
||||
when:
|
||||
notVisible:
|
||||
id: app-root
|
||||
commands:
|
||||
- tapOn:
|
||||
text: '.*(Add|Done|Select|Open|OK).*'
|
||||
optional: true
|
||||
- tapOn:
|
||||
point: '90%,92%'
|
||||
- tapOn:
|
||||
id: 'com\.(google\.)?android\.providers\.media\.module:id/icon_thumbnail'
|
||||
index: 0
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: 'com\.(google\.)?android\.providers\.media\.module:id/button_add'
|
||||
timeout: 10000
|
||||
- tapOn:
|
||||
id: 'com\.(google\.)?android\.providers\.media\.module:id/button_add'
|
||||
|
||||
# The picker must be closed before the rest of the flow can continue.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: app-root
|
||||
|
|
|
|||
|
|
@ -1,15 +1,5 @@
|
|||
appId: ${APP_ID || "in.freshyo.adminui"}
|
||||
---
|
||||
# Sets the slot's delivery (+60 min) and freeze (+50 min) date/times using the
|
||||
# native Android pickers. Assumes the create-slot screen is already open.
|
||||
- evalScript: ${output.delivery = new Date(Date.now() + 60 * 60 * 1000)}
|
||||
- evalScript: ${output.freeze = new Date(Date.now() + 50 * 60 * 1000)}
|
||||
- evalScript: ${output.deliveryHour = ('0' + output.delivery.getHours()).slice(-2)}
|
||||
- evalScript: ${output.deliveryMinute = ('0' + output.delivery.getMinutes()).slice(-2)}
|
||||
- evalScript: ${output.freezeHour = ('0' + output.freeze.getHours()).slice(-2)}
|
||||
- evalScript: ${output.freezeMinute = ('0' + output.freeze.getMinutes()).slice(-2)}
|
||||
|
||||
# --- Delivery: date (today) + time (+60) ---
|
||||
- tapOn:
|
||||
id: delivery-date-picker
|
||||
- tapOn:
|
||||
|
|
@ -23,15 +13,14 @@ appId: ${APP_ID || "in.freshyo.adminui"}
|
|||
- tapOn:
|
||||
id: 'android:id/input_hour'
|
||||
- eraseText
|
||||
- inputText: ${output.deliveryHour}
|
||||
- inputText: ${SLOT_HOUR}
|
||||
- tapOn:
|
||||
id: 'android:id/input_minute'
|
||||
- eraseText
|
||||
- inputText: ${output.deliveryMinute}
|
||||
- inputText: ${SLOT_MIN}
|
||||
- tapOn:
|
||||
id: 'android:id/button1'
|
||||
|
||||
# --- Freeze: date (today) + time (+50) ---
|
||||
- tapOn:
|
||||
id: freeze-date-picker
|
||||
- tapOn:
|
||||
|
|
@ -45,10 +34,10 @@ appId: ${APP_ID || "in.freshyo.adminui"}
|
|||
- tapOn:
|
||||
id: 'android:id/input_hour'
|
||||
- eraseText
|
||||
- inputText: ${output.freezeHour}
|
||||
- inputText: ${SLOT_FREEZE_HOUR}
|
||||
- tapOn:
|
||||
id: 'android:id/input_minute'
|
||||
- eraseText
|
||||
- inputText: ${output.freezeMinute}
|
||||
- inputText: ${SLOT_FREEZE_MIN}
|
||||
- tapOn:
|
||||
id: 'android:id/button1'
|
||||
|
|
|
|||
119
tests/e2e/user-order.yaml
Normal file
119
tests/e2e/user-order.yaml
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
appId: ${USER_APP_ID}
|
||||
env:
|
||||
USER_APP_ID: ${USER_APP_ID || "in.freshyo.app"}
|
||||
USER_MOBILE: ${USER_MOBILE || ""}
|
||||
USER_PASSWORD: ${USER_PASSWORD || ""}
|
||||
PRODUCT_NAME: ${PRODUCT_NAME || "E2E Test Product"}
|
||||
SLOT_DATE: ${SLOT_DATE || ""}
|
||||
SLOT_HOUR: ${SLOT_HOUR || ""}
|
||||
SLOT_MIN: ${SLOT_MIN || ""}
|
||||
tags:
|
||||
- user
|
||||
- smoke
|
||||
---
|
||||
- launchApp:
|
||||
clearState: true
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: 'Next|Home'
|
||||
timeout: 60000
|
||||
|
||||
- tapOn:
|
||||
text: 'Next'
|
||||
optional: true
|
||||
- waitForAnimationToEnd
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: home-search
|
||||
timeout: 30000
|
||||
- tapOn:
|
||||
id: home-search
|
||||
- tapOn:
|
||||
id: search-input-input
|
||||
- inputText: ${PRODUCT_NAME}
|
||||
- pressKey: enter
|
||||
- tapOn: 'Add to Cart'
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: 'slot-${SLOT_DATE}-${SLOT_HOUR}-${SLOT_MIN}'
|
||||
timeout: 20000
|
||||
- tapOn:
|
||||
id: 'slot-${SLOT_DATE}-${SLOT_HOUR}-${SLOT_MIN}'
|
||||
- tapOn:
|
||||
id: add-to-cart-confirm
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: go-to-cart
|
||||
timeout: 20000
|
||||
- tapOn:
|
||||
id: go-to-cart
|
||||
- scrollUntilVisible:
|
||||
element:
|
||||
id: checkout-button
|
||||
direction: DOWN
|
||||
- tapOn:
|
||||
id: checkout-button
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: 'user-login-mobile|place-order-button'
|
||||
timeout: 30000
|
||||
- runFlow:
|
||||
when:
|
||||
visible:
|
||||
id: user-login-mobile
|
||||
commands:
|
||||
- runFlow: subflows/login-user.yaml
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: home-search
|
||||
timeout: 60000
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: go-to-cart
|
||||
timeout: 20000
|
||||
- tapOn:
|
||||
id: go-to-cart
|
||||
- scrollUntilVisible:
|
||||
element:
|
||||
id: checkout-button
|
||||
direction: DOWN
|
||||
- tapOn:
|
||||
id: checkout-button
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: address-option-0
|
||||
timeout: 30000
|
||||
- scrollUntilVisible:
|
||||
element:
|
||||
id: place-order-button
|
||||
direction: DOWN
|
||||
- runFlow:
|
||||
when:
|
||||
visible:
|
||||
id: select-address-error
|
||||
commands:
|
||||
- scrollUntilVisible:
|
||||
element:
|
||||
id: address-option-0
|
||||
direction: UP
|
||||
- tapOn:
|
||||
id: address-option-0
|
||||
- scrollUntilVisible:
|
||||
element:
|
||||
id: place-order-button
|
||||
direction: DOWN
|
||||
|
||||
- extendedWaitUntil:
|
||||
notVisible:
|
||||
id: select-address-error
|
||||
timeout: 15000
|
||||
- tapOn:
|
||||
id: place-order-button
|
||||
|
||||
- assertVisible: 'Order Placed Successfully!'
|
||||
Loading…
Add table
Reference in a new issue