diff --git a/apps/admin-ui/app/(drawer)/dashboard/slots/index.tsx b/apps/admin-ui/app/(drawer)/dashboard/slots/index.tsx index 9ac862b..9c82636 100644 --- a/apps/admin-ui/app/(drawer)/dashboard/slots/index.tsx +++ b/apps/admin-ui/app/(drawer)/dashboard/slots/index.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { MaterialCommunityIcons, Entypo } from '@expo/vector-icons'; import { View, TouchableOpacity, FlatList, Alert, ActivityIndicator } from 'react-native'; -import { AppContainer, MyText, tw, MyFlatList , BottomDialog, MyTouchableOpacity, Checkbox } from 'common-ui'; +import { MyText, tw, MyFlatList , BottomDialog, MyTouchableOpacity, Checkbox } from 'common-ui'; import { trpc } from '@/src/trpc-client'; import { useRouter } from 'expo-router'; import dayjs from 'dayjs'; diff --git a/change-log.txt b/change-log.txt index bec9267..4049f6e 100644 --- a/change-log.txt +++ b/change-log.txt @@ -2280,6 +2280,113 @@ Confirmed from AOSP MediaProvider res/layout/activity_photo_picker.xml: guard so they can only run while the picker is still open (previously the text regex could have tapped "Add Variant" in the app after the picker closed) +[2026-09-16 00:05:33] Add slot E2E flow (create slot with relative times holding the product) + verbatim product name. + +Product name is now used verbatim from .env (no timestamp suffix) so the slot flow can +search for the exact product created by add-product.yaml. + +=== apps/admin-ui/app/(drawer)/dashboard/slots/index.tsx === +- import: removed unused `AppContainer` from the common-ui import + (the list already renders + , so no nested scroll) + +=== tests/e2e/run.sh === +- removed `PRODUCT_NAME="${PRODUCT_NAME} $(date +%s)"` (and its export) — name used verbatim + +=== tests/e2e/.env.example === +- PRODUCT_NAME comment updated: exact name, must be unique per run + +=== tests/e2e/config.yaml === +- added executionOrder (continueOnFailure: false; flowsOrder: add-product, add-slot-with-product) + +=== tests/e2e/add-slot-with-product.yaml (NEW) === +- login -> add-slot-fab +- evalScript computes delivery(+60m) and freeze(+50m) HH/MM +- delivery/freeze date via -date-picker -> android:id/button1 +- delivery/freeze time via -time-picker -> android:id/toggle_mode (optional) + -> android:id/input_hour / input_minute (eraseText + inputText) -> android:id/button1 +- products via slot-products-selector -> slot-products-selector-search -> inputText ${PRODUCT_NAME} + -> slot-products-selector-option-.* (index 0,1) -> slot-products-selector-done +- create-slot-button -> assert "Slot created successfully!" -> OK + +=== tests/e2e/README.md === +- documented the slot flow, suite ordering, and the verbatim-name caveat + +[2026-09-16 00:08:22] Extract slot time-picking into a subflow + add a standalone empty-slot flow. + +Purpose: validate the native date/time picker interaction in isolation, without +creating a product first. + +=== tests/e2e/subflows/set-slot-times.yaml (NEW) === +- computes delivery(+60m)/freeze(+50m) HH/MM via evalScript and drives the native + pickers (delivery/freeze date-picker -> android:id/button1; time-picker -> + android:id/toggle_mode (optional) -> input_hour / input_minute -> button1) + extracted verbatim from add-slot-with-product.yaml + +=== tests/e2e/add-slot-with-product.yaml === +- replaced the inline evalScript + delivery/freeze picker blocks with + `runFlow: subflows/set-slot-times.yaml` + +=== tests/e2e/dev/add-empty-slot.yaml (NEW) === +- standalone flow: login -> add-slot-fab -> set-slot-times -> create-slot-button + -> assert "Slot created successfully!" (no products) +- lives in dev/ so the root `*.yaml` suite glob does not pick it up + +[2026-09-16 00:11:19] Fix slot flows: navigate to the Slots list before the FAB. + +Symptom: `Element not found: Id matching regex: add-slot-fab`. The FAB is on the +Slots list screen, but the flows tapped it right after login (which lands on the +dashboard). + +=== tests/e2e/add-slot-with-product.yaml === +- added: scrollUntilVisible delivery-slots-menu-item -> tapOn delivery-slots-menu-item + -> assertVisible add-slot-fab, before tapping add-slot-fab + +=== tests/e2e/dev/add-empty-slot.yaml === +- same navigation added after login + +[2026-09-16 00:21:09] Make login subflow idempotent; relaunch app between suite flows. + +Symptom: after add-product succeeded (already authenticated, not on the login +screen), add-slot-with-product failed at `Assertion is false: id: login-name-input +is visible`. + +=== tests/e2e/subflows/login.yaml === +- wrapped the login steps in `runFlow: when: visible id: login-name-input`, so the + subflow logs in only if the login screen is showing (reusable standalone or after + another already-authenticated flow) +- removed the trailing `assertVisible add-product-menu-item` (callers wait for their + own first element) + +=== tests/e2e/add-slot-with-product.yaml === +- added `launchApp` before login so the app restarts at its initial route + (dashboard when already logged in, login otherwise) + +=== tests/e2e/dev/add-empty-slot.yaml === +- added `launchApp` before login for the same reason + +[2026-09-16 00:22:43] Keep login.yaml as a real flow; make the invocation conditional. + +Rework of the previous entry: login.yaml is a normal (unconditional) login flow +again; callers invoke it only when the login screen is showing. + +=== tests/e2e/subflows/login.yaml === +- restored unconditional login steps + trailing `assertVisible add-product-menu-item` + (no inner `when`) + +=== tests/e2e/add-product.yaml, add-slot-with-product.yaml, dev/add-empty-slot.yaml === +- replaced `runFlow: ` with a conditional inline wrapper: + - runFlow: + when: + visible: + id: login-name-input + commands: + - runFlow: + + + + + + diff --git a/tests/e2e/.env.example b/tests/e2e/.env.example index 24d3db3..07d7fc7 100644 --- a/tests/e2e/.env.example +++ b/tests/e2e/.env.example @@ -9,8 +9,9 @@ APP_ID=in.freshyo.adminui STAFF_NAME= STAFF_PASSWORD= -# Product under test. run.sh appends a timestamp so re-runs don't collide with -# the backend's unique-name check. +# 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) diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 1d50237..8cea4a9 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -6,13 +6,17 @@ Maestro flows that drive the **admin-ui** Expo app on an Android emulator. ``` tests/e2e/ - config.yaml # workspace config (entry flows = *.yaml at the root) - add-product.yaml # first flow: create a product with 2 SKUs (one flash) + 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 subflows/ - login.yaml # reusable login + 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 + 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` - run.sh # sources .env and runs `maestro test` + run.sh # loads .env and runs `maestro test` .env.example # copy to .env and fill in ``` @@ -33,11 +37,14 @@ cp tests/e2e/.env.example tests/e2e/.env ## Run ```bash -bash tests/e2e/run.sh # every entry flow in tests/e2e/ +bash tests/e2e/run.sh # every entry flow (ordered: add-product, then add-slot-with-product) bash tests/e2e/run.sh add-product.yaml +bash tests/e2e/run.sh dev/add-empty-slot.yaml # standalone: slot only, no product ``` -`run.sh` appends a timestamp to `PRODUCT_NAME` (the backend rejects duplicate names). +`PRODUCT_NAME` is used **verbatim** — `add-product.yaml` creates it with that exact name +and `add-slot-with-product.yaml` searches for it. The backend rejects duplicate names, so +change `PRODUCT_NAME` (or delete the product) between suite runs. ## What `add-product.yaml` does @@ -49,14 +56,26 @@ bash tests/e2e/run.sh add-product.yaml 6. Adds Variant 2, fills quantity/prices, toggles **Flash Available**, sets flash price, attaches an image. 7. Submits and asserts `Product created successfully!`. -All in-app interactions use `testID` selectors (`id:`). The only exception is the -OS photo picker, which is system UI and is targeted by its platform IDs in -`subflows/pick-image.yaml`. +## What `add-slot-with-product.yaml` does + +1. `evalScript` computes delivery = now + 60 min and freeze = now + 50 min. +2. Logs in and taps the `add-slot-fab`. +3. Sets the delivery/freeze date (today) and time via the native Android pickers + (`android:id/button1`, `toggle_mode`, `input_hour`, `input_minute`). +4. Opens `slot-products-selector`, searches the exact `PRODUCT_NAME`, selects the + two SKU options and taps Done. +5. Submits and asserts `Slot created successfully!`. + +All in-app interactions use `testID` selectors (`id:`). The exceptions are system UI — +the OS photo picker (`subflows/pick-image.yaml`) and the native date/time pickers. ## Notes / gotchas - The image picker flow is the most version-sensitive part; thumbs are targeted by the Android 13/14 Photo Picker id with a DocumentsUI fallback, and the confirm - button by text regex `Add|Done|Select`. + button by its `button_add` id. +- Native time-picker ids (`android:id/toggle_mode`, `input_hour`, `input_minute`, + `button1`) depend on the OS version. The date dialog defaults to today, so a run + within ~60 min of midnight would put delivery on the next day. - On a reused emulator, `addMedia` images accumulate in the gallery (harmless). - Tests create real data on the connected backend. diff --git a/tests/e2e/add-product.yaml b/tests/e2e/add-product.yaml index 0ec3ba9..3de8d32 100644 --- a/tests/e2e/add-product.yaml +++ b/tests/e2e/add-product.yaml @@ -28,8 +28,13 @@ tags: permissions: storage: allow -# --- Login --- -- runFlow: subflows/login.yaml +# --- Login (only when the login screen is showing) --- +- runFlow: + when: + visible: + id: login-name-input + commands: + - runFlow: subflows/login.yaml # --- Navigate to Add Product --- - tapOn: diff --git a/tests/e2e/add-slot-with-product.yaml b/tests/e2e/add-slot-with-product.yaml new file mode 100644 index 0000000..caa7281 --- /dev/null +++ b/tests/e2e/add-slot-with-product.yaml @@ -0,0 +1,69 @@ +appId: ${APP_ID || "in.freshyo.adminui"} +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"} +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). +- runFlow: + when: + visible: + id: login-name-input + 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 + +# --- 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 + direction: DOWN +- tapOn: + id: slot-products-selector +- tapOn: + id: slot-products-selector-search +- inputText: ${PRODUCT_NAME} +- hideKeyboard +- tapOn: + id: 'slot-products-selector-option-.*' + index: 0 +- tapOn: + id: 'slot-products-selector-option-.*' + index: 1 +- tapOn: + id: slot-products-selector-done + +# --- Submit --- +- scrollUntilVisible: + element: + id: create-slot-button + direction: DOWN +- tapOn: + id: create-slot-button + +- assertVisible: 'Slot created successfully!' +- tapOn: 'OK' diff --git a/tests/e2e/config.yaml b/tests/e2e/config.yaml index c9b4859..e3f6cf7 100644 --- a/tests/e2e/config.yaml +++ b/tests/e2e/config.yaml @@ -4,3 +4,10 @@ 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 diff --git a/tests/e2e/dev/add-empty-slot.yaml b/tests/e2e/dev/add-empty-slot.yaml new file mode 100644 index 0000000..c860821 --- /dev/null +++ b/tests/e2e/dev/add-empty-slot.yaml @@ -0,0 +1,47 @@ +appId: ${APP_ID || "in.freshyo.adminui"} +env: + APP_ID: ${APP_ID || "in.freshyo.adminui"} + STAFF_NAME: ${STAFF_NAME || ""} + STAFF_PASSWORD: ${STAFF_PASSWORD || ""} +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. +- runFlow: + when: + visible: + id: login-name-input + 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 + +- runFlow: ../subflows/set-slot-times.yaml + +- scrollUntilVisible: + element: + id: create-slot-button + direction: DOWN +- tapOn: + id: create-slot-button + +- assertVisible: 'Slot created successfully!' +- tapOn: 'OK' diff --git a/tests/e2e/run.sh b/tests/e2e/run.sh index d04e320..6cd96d2 100755 --- a/tests/e2e/run.sh +++ b/tests/e2e/run.sh @@ -32,8 +32,8 @@ done < ./.env : "${PRODUCT_NAME:?PRODUCT_NAME is required}" : "${STORE_ID:?STORE_ID is required}" -# Unique product name so repeat runs don't trip the backend's duplicate check. -PRODUCT_NAME="${PRODUCT_NAME} $(date +%s)" +# 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. diff --git a/tests/e2e/subflows/login.yaml b/tests/e2e/subflows/login.yaml index d8753e4..7b4f282 100644 --- a/tests/e2e/subflows/login.yaml +++ b/tests/e2e/subflows/login.yaml @@ -3,9 +3,10 @@ env: STAFF_NAME: ${STAFF_NAME || ""} STAFF_PASSWORD: ${STAFF_PASSWORD || ""} --- -# Logs into the admin app. Reused by other flows via `runFlow: subflows/login.yaml`. -- assertVisible: - id: login-name-input +# 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} @@ -15,6 +16,6 @@ env: - hideKeyboard - tapOn: id: login-button -# Dashboard quick actions are only rendered once the drawer shell is up. +# Dashboard quick actions render once the drawer shell is up. - assertVisible: id: add-product-menu-item diff --git a/tests/e2e/subflows/set-slot-times.yaml b/tests/e2e/subflows/set-slot-times.yaml new file mode 100644 index 0000000..2e9e818 --- /dev/null +++ b/tests/e2e/subflows/set-slot-times.yaml @@ -0,0 +1,54 @@ +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: + id: 'android:id/button1' + +- tapOn: + id: delivery-time-picker +- tapOn: + id: 'android:id/toggle_mode' + optional: true +- tapOn: + id: 'android:id/input_hour' +- eraseText +- inputText: ${output.deliveryHour} +- tapOn: + id: 'android:id/input_minute' +- eraseText +- inputText: ${output.deliveryMinute} +- tapOn: + id: 'android:id/button1' + +# --- Freeze: date (today) + time (+50) --- +- tapOn: + id: freeze-date-picker +- tapOn: + id: 'android:id/button1' + +- tapOn: + id: freeze-time-picker +- tapOn: + id: 'android:id/toggle_mode' + optional: true +- tapOn: + id: 'android:id/input_hour' +- eraseText +- inputText: ${output.freezeHour} +- tapOn: + id: 'android:id/input_minute' +- eraseText +- inputText: ${output.freezeMinute} +- tapOn: + id: 'android:id/button1'