Phase 3.7: paid pairing flow + returning chat + extension flip
- Backend: payment_sessions + pairing_failures tables; payment.service.js and pairing-failure.service.js (new); rewritten pairing.service.js (payment-gated blast + targeted "Curhat lagi" + cancel + fallback); rewritten extension.service.js (data-driven auto-approve with offline safeguard, charge-at-approval); pricing.service.js (extension tiers without free trial); mitra-status.service.js (countAvailableMitras cached path); 60s sweeper for stale payment sessions - Backend routes: client.payment.routes, client.mitra-availability.routes, internal/failed-pairings.routes; client.chat.routes rewritten for payment-gated start + /returning + /cancel + /fallback-to-blast; internal/config.routes adds 4 new keys with Valkey invalidate publish - client_app: mitra-availability poll, payment screen + notifier, pairing notifier rewrite (PairingTargetedWaiting + PairingFailed states), targeted-waiting overlay + bestie-unavailable dialog, "Curhat lagi" CTA, failed-pairing terminal, extension via payment-session - mitra_app: PairingRequestType enum, returning-chat 20s countdown auto-dismiss, extension card "otomatis disetujui" copy - control_center: 4 new config rows in Settings, Failed Pairings page (filter + paginate + action menu), sidebar + route registered - Test infrastructure: Vitest backend (7/7 pass), Playwright CC (4/4 pass), Maestro mobile scaffold (CLI install pending) - Bugs found via Playwright + fixed: LoginPage labels not associated with inputs (a11y); backend internal CORS missing PATCH/PUT/DELETE in allow-methods (silent settings breakage in browsers since Stage 4) - Docs: phase3.7.md PRD, phase3.7-plan.md, phase3.7-questions.md (Q&A), phase3.7-testing.md (E2E checklist), phase3.7-test-run-2026-05-03.md (today's run results) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
111
client_app/.maestro/README.md
Normal file
111
client_app/.maestro/README.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# client_app Maestro flows
|
||||
|
||||
End-to-end UI automation for the customer Flutter app using [Maestro](https://maestro.mobile.dev). Single-emulator + curl-as-mitra pattern — the customer app is driven by real Maestro touches; the mitra side is simulated via backend API calls fired from `runScript` steps.
|
||||
|
||||
## One-time install
|
||||
|
||||
Maestro is a global CLI (not a project dependency). Install on your dev machine once:
|
||||
|
||||
```bash
|
||||
curl -Ls "https://get.maestro.mobile.dev" | bash
|
||||
```
|
||||
|
||||
Verify with `maestro --version`. See the [Maestro install docs](https://maestro.mobile.dev/getting-started/installing-maestro) for Homebrew / chocolatey / Docker alternatives.
|
||||
|
||||
You also need:
|
||||
- `adb` on your PATH (comes with Android Studio's platform-tools)
|
||||
- `jq` for the helper scripts (`apt install jq` / `brew install jq`)
|
||||
- One Android emulator OR one connected device — **only one at a time** (per project decision)
|
||||
|
||||
## Folder layout
|
||||
|
||||
```
|
||||
.maestro/
|
||||
├── README.md # this file
|
||||
├── config.yaml # shared env: app IDs, backend URL, test credentials
|
||||
├── flows/ # the YAML test scripts
|
||||
│ ├── 01_smoke.yaml
|
||||
│ ├── 02_cta_disabled_when_no_mitra.yaml
|
||||
│ └── 03_payment_to_chat_happy.yaml
|
||||
└── scripts/ # bash helpers invoked by `runScript` steps
|
||||
├── mitra_accept_latest.sh
|
||||
└── force_all_mitras_offline.sh
|
||||
```
|
||||
|
||||
## Configure for your environment
|
||||
|
||||
Edit `.maestro/config.yaml` and fill in:
|
||||
- `BACKEND_URL` — must match the `--dart-define=API_BASE_URL=...` value the installed APK was built with
|
||||
- `TEST_MITRA_ID` and `TEST_MITRA_JWT` — used by the curl harness to "accept" requests from the customer's blast
|
||||
|
||||
The config file is committed because the values are dev-environment defaults. Sensitive credentials (real JWTs, CC operator tokens) should be passed at runtime instead — see "Per-machine overrides" below.
|
||||
|
||||
## Run a flow
|
||||
|
||||
Single emulator (typical case — Maestro auto-picks the only attached device):
|
||||
|
||||
```bash
|
||||
# from repo root or anywhere
|
||||
maestro test client_app/.maestro/flows/01_smoke.yaml
|
||||
|
||||
# or run all flows in the directory
|
||||
maestro test client_app/.maestro/flows/
|
||||
```
|
||||
|
||||
If both an emulator and a real device happen to be connected, list them and pick one explicitly:
|
||||
|
||||
```bash
|
||||
adb devices # list attached devices
|
||||
maestro --device emulator-5554 test client_app/.maestro/flows/01_smoke.yaml
|
||||
```
|
||||
|
||||
## Per-machine overrides
|
||||
|
||||
Override any config.yaml value at runtime with `--env`:
|
||||
|
||||
```bash
|
||||
maestro test \
|
||||
--env BACKEND_URL=http://192.168.99.10:3000 \
|
||||
--env TEST_MITRA_JWT=eyJhbGc... \
|
||||
client_app/.maestro/flows/03_payment_to_chat_happy.yaml
|
||||
```
|
||||
|
||||
Or export shell variables — `runScript` steps inherit them:
|
||||
|
||||
```bash
|
||||
export CC_JWT=eyJhbGc...
|
||||
maestro test client_app/.maestro/flows/02_cta_disabled_when_no_mitra.yaml
|
||||
```
|
||||
|
||||
## Single-emulator + curl pattern
|
||||
|
||||
Phase 3.7 flows often need a customer + a mitra acting in concert. Instead of running two emulators (RAM-heavy, flaky), the flows drive the customer side with Maestro and **simulate the mitra via backend curl calls**:
|
||||
|
||||
1. Maestro flow drives customer up to the "Mencari Bestie..." state
|
||||
2. `runScript: ../scripts/mitra_accept_latest.sh` fires `POST /api/mitra/chat-requests/:id/accept` against the backend, using a pre-minted mitra JWT
|
||||
3. Maestro flow asserts the customer screen transitions to "Bestie Ditemukan" via the WS round-trip
|
||||
|
||||
This works for ~90% of multi-actor scenarios — including all the Section D ("Curhat lagi") and Section J ("Mitra goes offline mid-session") tests in [phase3.7-testing.md](../../requirement/phase3.7-testing.md). The 10% that needs both UIs running (e.g., asserting the mitra-side overlay countdown displays correctly) is in [`mitra_app/.maestro/`](../../mitra_app/.maestro/) and runs separately.
|
||||
|
||||
## Adding a new flow
|
||||
|
||||
Pick a Phase 3.7 testing checklist scenario (see [phase3.7-testing.md](../../requirement/phase3.7-testing.md)), then:
|
||||
|
||||
1. Copy an existing flow as a template (e.g., `03_payment_to_chat_happy.yaml`)
|
||||
2. Update the pre-req comment, the steps, and the assertions
|
||||
3. If you need a "second actor" action, add a bash helper under `scripts/` and call it via `runScript:`
|
||||
4. If you need new env vars, add them to `config.yaml` with sensible defaults
|
||||
|
||||
## Tips
|
||||
|
||||
- **Find the right text to tap on** — `maestro studio` opens a live UI inspector showing every visible label/widget. Run it while the app is on the screen you care about.
|
||||
- **Slow it down for debugging** — `maestro test --debug-output ./debug flows/foo.yaml` saves screenshots + logs per step.
|
||||
- **Add flows incrementally** — Maestro's reload-on-save in `maestro studio` makes iteration fast.
|
||||
- **Don't commit screenshots / debug output** — add `.maestro/output/` and `.maestro/screenshots/` to `.gitignore` if you generate them locally.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- `maestro: device not found` → run `adb devices`; if empty, start an emulator (`emulator -avd <name>`) or plug in a USB device with debugging enabled.
|
||||
- `Element not visible` errors → use `maestro studio` to inspect actual labels — they may have changed since the flow was written.
|
||||
- Flow hangs at `assertVisible` waiting for backend → check `BACKEND_URL` matches the APK's build-time value (`grep API_BASE_URL build.gradle`).
|
||||
- `runScript` exits non-zero → run the script directly to see its error: `bash client_app/.maestro/scripts/mitra_accept_latest.sh`. Most often a missing env var or stale JWT.
|
||||
25
client_app/.maestro/config.yaml
Normal file
25
client_app/.maestro/config.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
# Shared variables for all client_app Maestro flows.
|
||||
#
|
||||
# Override at runtime with `maestro test --env KEY=value` or by setting shell env vars.
|
||||
# See README.md for full setup + per-machine overrides.
|
||||
|
||||
env:
|
||||
# App identifiers — Android / iOS bundle IDs picked up automatically by `appId:` in flows.
|
||||
APP_ID_ANDROID: com.halobestie.client.client_app
|
||||
APP_ID_IOS: com.halobestie.client.clientApp
|
||||
|
||||
# Backend the app talks to — must match what the installed APK was built with
|
||||
# (the `--dart-define=API_BASE_URL=...` value at build time).
|
||||
BACKEND_URL: http://192.168.88.247:3000
|
||||
BACKEND_INTERNAL_URL: http://192.168.88.247:3001
|
||||
|
||||
# Test customer credentials — must exist in the customers table on the target backend.
|
||||
# These are read by helper scripts (see .maestro/scripts/) when seeding state.
|
||||
CUSTOMER_PHONE: "+628100000001"
|
||||
CUSTOMER_OTP: "123456" # OTP stub mode emits a known code per phone
|
||||
|
||||
# If you need to drive a "second actor" (e.g., the mitra accepting a blast), the test
|
||||
# flows curl the backend directly using these credentials. See README §"Single-emulator
|
||||
# + curl pattern" for details.
|
||||
TEST_MITRA_ID: "REPLACE-WITH-A-REAL-MITRA-UUID"
|
||||
TEST_MITRA_JWT: "REPLACE-WITH-A-VALID-MITRA-JWT"
|
||||
14
client_app/.maestro/flows/01_smoke.yaml
Normal file
14
client_app/.maestro/flows/01_smoke.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
# Smoke test: launch the app and assert the home screen renders.
|
||||
# Use this flow first to verify Maestro can talk to your device/emulator at all.
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/01_smoke.yaml
|
||||
#
|
||||
# Pre-req: client_app debug APK installed on the connected device, signed in as a customer.
|
||||
appId: ${APP_ID_ANDROID} # Maestro uses APP_ID_IOS automatically when running against an iOS device
|
||||
---
|
||||
- launchApp:
|
||||
clearState: false # keep existing auth — set to true to test cold-start onboarding
|
||||
- assertVisible:
|
||||
text: "Mulai Curhat"
|
||||
timeout: 10000 # 10s — give Riverpod time to hydrate the home screen
|
||||
21
client_app/.maestro/flows/02_cta_disabled_when_no_mitra.yaml
Normal file
21
client_app/.maestro/flows/02_cta_disabled_when_no_mitra.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
# Verifies the home CTA is disabled and shows the "no bestie available" subtitle
|
||||
# when no mitra is online.
|
||||
#
|
||||
# Pre-req: NO mitras are online on the target backend. Use the helper script to
|
||||
# force everyone offline before running:
|
||||
# bash client_app/.maestro/scripts/force_all_mitras_offline.sh
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/02_cta_disabled_when_no_mitra.yaml
|
||||
appId: ${APP_ID_ANDROID}
|
||||
---
|
||||
- launchApp:
|
||||
clearState: false
|
||||
- assertVisible: "Mulai Curhat"
|
||||
- assertVisible: "Belum ada bestie tersedia"
|
||||
# CTA is disabled — tapping it should be a no-op.
|
||||
- tapOn: "Mulai Curhat"
|
||||
# We should still be on the home screen, NOT on the payment screen.
|
||||
- assertNotVisible: "Bayar"
|
||||
- assertNotVisible: "Mulai"
|
||||
- assertVisible: "Mulai Curhat"
|
||||
42
client_app/.maestro/flows/03_payment_to_chat_happy.yaml
Normal file
42
client_app/.maestro/flows/03_payment_to_chat_happy.yaml
Normal file
@@ -0,0 +1,42 @@
|
||||
# Happy-path golden flow: customer taps CTA → payment screen → confirm →
|
||||
# searching → mitra accepts (via curl harness) → chat screen.
|
||||
#
|
||||
# This is the canonical demonstration of the "single-emulator + curl-as-mitra"
|
||||
# pattern. The customer side is real Maestro; the mitra side is a backend API call
|
||||
# fired from a runScript step.
|
||||
#
|
||||
# Pre-req:
|
||||
# 1. At least one mitra is ONLINE on the target backend
|
||||
# 2. TEST_MITRA_ID and TEST_MITRA_JWT in .maestro/config.yaml point at that mitra
|
||||
# 3. The mitra has spare capacity (active_session_count < max_customers_per_mitra)
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/03_payment_to_chat_happy.yaml
|
||||
appId: ${APP_ID_ANDROID}
|
||||
---
|
||||
- launchApp:
|
||||
clearState: false
|
||||
- assertVisible: "Mulai Curhat"
|
||||
|
||||
# Step 1: customer taps CTA → payment screen
|
||||
- tapOn: "Mulai Curhat"
|
||||
- assertVisible:
|
||||
text: "Bayar|Mulai" # "Bayar" for paid tier, "Mulai" for free trial
|
||||
timeout: 5000
|
||||
|
||||
# Step 2: customer confirms payment → searching screen
|
||||
- tapOn:
|
||||
text: "Bayar|Mulai"
|
||||
- assertVisible:
|
||||
text: "Mencari Bestie"
|
||||
timeout: 5000
|
||||
|
||||
# Step 3: simulate mitra accepting via backend API (the "curl-as-mitra" trick).
|
||||
# This avoids needing a second emulator — the backend treats mitra interactions as
|
||||
# REST calls regardless of whether they originate from the mitra app or a script.
|
||||
- runScript: ../scripts/mitra_accept_latest.sh
|
||||
|
||||
# Step 4: customer screen transitions to "found" then chat
|
||||
- assertVisible:
|
||||
text: "Bestie Ditemukan"
|
||||
timeout: 10000 # blast→accept→WS round-trip takes a few seconds
|
||||
34
client_app/.maestro/scripts/force_all_mitras_offline.sh
Executable file
34
client_app/.maestro/scripts/force_all_mitras_offline.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# Force every mitra offline on the target backend. Used as a pre-step for tests
|
||||
# that verify the "no bestie available" disabled-CTA state.
|
||||
#
|
||||
# Reads BACKEND_INTERNAL_URL and a CC_JWT from the shell env (NOT from
|
||||
# .maestro/config.yaml — CC credentials should never be committed).
|
||||
#
|
||||
# Usage:
|
||||
# CC_JWT=<token> bash client_app/.maestro/scripts/force_all_mitras_offline.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${BACKEND_INTERNAL_URL:=http://192.168.88.247:3001}"
|
||||
: "${CC_JWT:?CC_JWT must be set (a valid control_center user JWT)}"
|
||||
|
||||
# Get the list of currently online mitras from the CC dashboard endpoint.
|
||||
mitras=$(curl -fsSL "$BACKEND_INTERNAL_URL/internal/mitra-online-status" \
|
||||
-H "Authorization: Bearer $CC_JWT" \
|
||||
| jq -r '.data[] | select(.is_online == true) | .mitra_id')
|
||||
|
||||
if [ -z "$mitras" ]; then
|
||||
echo "All mitras already offline."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for mitra_id in $mitras; do
|
||||
echo "Forcing $mitra_id offline..."
|
||||
curl -fsSL -X POST "$BACKEND_INTERNAL_URL/internal/mitra-online-status/$mitra_id/offline" \
|
||||
-H "Authorization: Bearer $CC_JWT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}' || echo " (failed — endpoint may not exist; check route name)"
|
||||
done
|
||||
|
||||
echo "Done."
|
||||
36
client_app/.maestro/scripts/mitra_accept_latest.sh
Executable file
36
client_app/.maestro/scripts/mitra_accept_latest.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# Find the most recent pending chat_session for the test mitra and accept it via
|
||||
# the backend API. Used by Maestro flows that drive the customer side and need a
|
||||
# mitra to "accept" without running a second app.
|
||||
#
|
||||
# Reads from .maestro/config.yaml env (BACKEND_URL, TEST_MITRA_ID, TEST_MITRA_JWT).
|
||||
# Maestro injects these as shell env vars before running this script.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${BACKEND_URL:?BACKEND_URL must be set in .maestro/config.yaml}"
|
||||
: "${TEST_MITRA_ID:?TEST_MITRA_ID must be set in .maestro/config.yaml}"
|
||||
: "${TEST_MITRA_JWT:?TEST_MITRA_JWT must be set in .maestro/config.yaml}"
|
||||
|
||||
# Poll for a pending request (blast may take 1-2 seconds to arrive)
|
||||
for i in 1 2 3 4 5; do
|
||||
pending=$(curl -fsSL "$BACKEND_URL/api/mitra/chat-requests/pending" \
|
||||
-H "Authorization: Bearer $TEST_MITRA_JWT" 2>/dev/null || echo '{"data":[]}')
|
||||
session_id=$(echo "$pending" | jq -r '.data[0].session_id // empty')
|
||||
if [ -n "$session_id" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [ -z "${session_id:-}" ]; then
|
||||
echo "ERROR: no pending chat request found for mitra $TEST_MITRA_ID after 5s" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Accepting session $session_id as mitra $TEST_MITRA_ID..."
|
||||
curl -fsSL -X POST "$BACKEND_URL/api/mitra/chat-requests/$session_id/accept" \
|
||||
-H "Authorization: Bearer $TEST_MITRA_JWT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
echo "OK"
|
||||
Reference in New Issue
Block a user