Phase 4 §4: payment-before-pair for returning users + Maestro suite
Stages 5.1, 5.3, 5.4 of the returning-user flow rework. All three §4 entry paths now require payment BEFORE pairing, matching the updated mermaid spec. * Spec (requirement/flow_customer.mermaid.md §4): payment block converges three call-sites (bestie-yang-udah-kenal-online, bestie-baru, offline-popup → cari bestie lain). PairRoute dispatches lama → targeted pair, baru/cari-lain → §3 blast. §3 retains its post-payment-shared contract. * Stage 5.1 (client_app): PaymentDraft carries targetedMitraId + topicSensitivity. bestie_history_list seeds the draft + pushes /payment/entry (was legacy /payment). searching_screen branches on draft.targetedMitraId for blast-vs-targeted dispatch. payment_entry uses resetExceptTarget(); bestie_choice_sheet + home _onCurhatBestieBaruPressed call explicit reset() before push so the keepAlive draft can't leak stale targeting into a blast. * Stage 5.3 (client_app): new BestieOfflineVariant.prePayReturning. Bestie-history-list _BestieRow splits tappable from dim so offline rows render dimmed but route taps into the popup. CTA "cari bestie lain" resets the draft + pushes /payment/entry. * Stage 5.4 (client_app): deleted legacy /payment route, payment_screen.dart, payment_notifier.dart(+.g.dart). router cleaned. * Tests (requirement/phase4-customer-flow.md + client_app/.maestro/): six Maestro flows TS-01..TS-06 covering every §4 branching point, all passing end-to-end. Shared onboarding prelude under .maestro/subflows/. New helper scripts: accept_latest_pending, force_mitra_offline, force_other_mitra_online, reset_all_mitras_online, mitra_accept_latest_internal. New backend _test endpoints to match. /reset-phone now cascade-deletes customer_transactions (FK was blocking). /force-pairing-timeout branches targeted (RETURNING_CHAT_TIMEOUT via expireTargetedPairingRequest, now exported) vs blast (PAIRING_FAILED). seed_history_session also outputs MITRA_NAME_RE (regex-escaped) for reliable selectors against display names containing regex specials. * mitra_app: dispose-during-deactivate guardrail for back-press on the mitra chat screen after the customer's goodbye message. Pending real emulator repro verification (carried over from 2026-05-15). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
155
client_app/.maestro/flows/ts-01_returning_lama_online.yaml
Normal file
155
client_app/.maestro/flows/ts-01_returning_lama_online.yaml
Normal file
@@ -0,0 +1,155 @@
|
||||
# TS-01 — Returning user re-pays an online bestie (lama happy path)
|
||||
# (requirement/phase4-customer-flow.md → Test Scenarios → TS-01).
|
||||
#
|
||||
# §4 branch covered: Choice → "bestie yang udah kenal" → CheckOnline(yes) →
|
||||
# PickMethod → PickDuration → PayMethod → Bayar → WaitPay → paid →
|
||||
# notif-gate → PairRoute(lama) → Targeted → accept → S10 chat.
|
||||
#
|
||||
# Pre-reqs (HARD — flow assumes these):
|
||||
# - Backend reachable (BACKEND_INTERNAL_URL).
|
||||
# - NODE_ENV != 'production' (so /internal/_test/* routes are registered).
|
||||
# - At least ONE mitra is currently online in dev DB. The dev seed
|
||||
# (backend/src/db/seed.js) does NOT auto-create mitras — ops must sign
|
||||
# in a test mitra via mitra_app + heartbeat (or insert
|
||||
# mitra_online_status manually). `seed_history_session.js` picks the
|
||||
# most-recently-online mitra; that same mitra must still be online when
|
||||
# the flow reaches the post-payment targeted-wait so
|
||||
# `mitra_accept_latest_internal.js` can drive accept.
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/ts-01_returning_lama_online.yaml
|
||||
appId: com.halobestie.client.client_app
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
---
|
||||
# --- Cold-start reset + onboarding prelude ---
|
||||
- runScript:
|
||||
file: ../scripts/reset_phone.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- runFlow: ../subflows/onboarding_returning_user.yaml
|
||||
|
||||
# --- Seed a completed chat_sessions row so the bestie history list isn't
|
||||
# empty. The seed picks the most-recently-online mitra; that mitra remains
|
||||
# online (this is the "lama" branch — TS-01 needs them online). ---
|
||||
- runScript:
|
||||
file: ../scripts/seed_history_session.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
# bestieHistoryProvider was already evaluated empty on the first home
|
||||
# render. Pull-to-refresh to re-fetch /chat/history so the seeded row shows
|
||||
# up and the CTA opens BestieChoiceSheet (instead of jumping straight to
|
||||
# /payment/entry, which is the no-history branch).
|
||||
- swipe:
|
||||
start: "50%, 30%"
|
||||
end: "50%, 80%"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*curhatan sebelumnya.*"
|
||||
timeout: 10000
|
||||
|
||||
# --- Step 1: tap CTA → Bestie Choice Sheet ---
|
||||
# After seeding, the CTA label flips from "aku mau curhat" (SHome1st) to
|
||||
# "curhat sama bestie baru" (SHomeReturning).
|
||||
- tapOn:
|
||||
text: "(?s).*curhat sama bestie baru.*"
|
||||
retryTapIfNoChange: true
|
||||
|
||||
# Sheet title "mau curhat sama siapa?" — `?` is regex meta; wrap in (?s).*…*
|
||||
# and use `.` for the trailing literal `?`.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*mau curhat sama siapa.*"
|
||||
timeout: 5000
|
||||
|
||||
# --- Step 2: pick "bestie yang udah kenal" → history list ---
|
||||
- tapOn: "(?s).*bestie yang udah kenal.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*bestie kamu sebelumnya.*"
|
||||
timeout: 5000
|
||||
|
||||
# --- Step 3: tap the seeded mitra row → /payment/entry → method-pick ---
|
||||
# Row label is "bestie <name>" + ONLINE pill (merged blob).
|
||||
- tapOn: "(?s).*bestie ${output.MITRA_NAME_RE}.*"
|
||||
|
||||
# /payment/entry is a routing shim; for a returning customer with prior
|
||||
# completed sessions the discount is NOT eligible, so it forwards to
|
||||
# /payment/method-pick (`pilih cara curhat`).
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*pilih cara curhat.*"
|
||||
timeout: 10000
|
||||
|
||||
# --- Step 4: pick chat mode → /payment/duration-pick ---
|
||||
- tapOn:
|
||||
text: "(?s).*tulis dan baca dengan tenang.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*pilih durasi.*"
|
||||
timeout: 10000
|
||||
|
||||
# --- Step 5: pick cheapest tier → CTA shows "bayar Rp X" ---
|
||||
# Tiers render via tier.label / formatRupiah; "5 Menit" is current label.
|
||||
- tapOn: "(?s).*5 Menit.*"
|
||||
- tapOn: "(?s).*bayar Rp.*"
|
||||
|
||||
# --- Step 6: cara-bayar screen → QRIS preselected → tap bayar ---
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*cara bayar.*"
|
||||
timeout: 10000
|
||||
- tapOn:
|
||||
text: "(?s).*bayar Rp.*"
|
||||
retryTapIfNoChange: true
|
||||
|
||||
# --- Step 7: waiting-payment screen → force-confirm ---
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "scan QRIS untuk bayar"
|
||||
timeout: 10000
|
||||
- runScript:
|
||||
file: ../scripts/mark_latest_payment_paid.js
|
||||
env:
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
# --- Step 8: notif-gate auto-advances (or "nanti aja" if shown) →
|
||||
# /chat/searching kicks off targeted-pair → routes to
|
||||
# /chat/waiting-targeted/<mitraId> ---
|
||||
# We accept either landing on notif-gate (if perm not yet decided) or
|
||||
# directly on targeted-waiting (if perm was already granted).
|
||||
- runFlow:
|
||||
when:
|
||||
visible:
|
||||
text: "(?s).*biar nggak ketinggalan.*"
|
||||
commands:
|
||||
- tapOn:
|
||||
text: "(?s).*nanti aja.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*MENUNGGU JAWABAN.*"
|
||||
timeout: 20000
|
||||
- assertVisible: "(?s).*lagi nungguin.*"
|
||||
|
||||
# --- Step 9: mitra-side accept via dev-only internal route ---
|
||||
- runScript:
|
||||
file: ../scripts/mitra_accept_latest_internal.js
|
||||
env:
|
||||
MITRA_ID: ${output.MITRA_ID}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
# --- Step 10: chat screen renders ---
|
||||
# Header shows mitra name + "online" status pill. The presence of either
|
||||
# the mitra name (post-accept) or the "online" indicator confirms we're on
|
||||
# the chat screen.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*online.*"
|
||||
timeout: 20000
|
||||
- assertVisible: "(?s).*${output.MITRA_NAME_RE}.*"
|
||||
@@ -0,0 +1,172 @@
|
||||
# TS-02 — Returning user picks offline bestie → "cari bestie lain" → blast
|
||||
# (requirement/phase4-customer-flow.md → Test Scenarios → TS-02).
|
||||
#
|
||||
# §4 branch covered: Choice → "bestie yang udah kenal" → CheckOnline(no) →
|
||||
# OfflinePopup(pre-pay) → "cari bestie lain" → PickMethod → … → paid →
|
||||
# PairRoute(baru) → BlastFlow → S10 chat.
|
||||
#
|
||||
# Pre-reqs (HARD):
|
||||
# - Backend reachable; NODE_ENV != 'production'.
|
||||
# - >= 2 mitras online in dev DB at flow start. The flow:
|
||||
# 1. `seed_history_session.js` picks the most-recently-online mitra
|
||||
# (call this M1) and creates a history row.
|
||||
# 2. `force_mitra_offline.js` forces M1 offline so the history list
|
||||
# renders dim.
|
||||
# 3. After the user takes the blast branch, a SECOND online mitra
|
||||
# (M2) is required to accept the blast via
|
||||
# `mitra_accept_latest_internal.js`.
|
||||
# If only one mitra exists, the blast will have no acceptor and step
|
||||
# 10's chat-screen assertion will time out.
|
||||
# - Dev seed (backend/src/db/seed.js) does NOT auto-create mitras — ops
|
||||
# must sign in >= 2 test mitras via mitra_app + heartbeat.
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/ts-02_returning_lama_offline_blast.yaml
|
||||
appId: com.halobestie.client.client_app
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
# Second online mitra that will accept the blast. Maestro flows currently
|
||||
# don't have a "pick any online mitra" helper, so we rely on
|
||||
# mitra-accept-latest taking the FIRST mitra that received the blast.
|
||||
# See "Open question" in the task spec — this is the "TS-02 needs two
|
||||
# online mitras" caveat.
|
||||
TEST_MITRA_ID_ACCEPTOR: "${TEST_MITRA_ID_ACCEPTOR}"
|
||||
---
|
||||
# --- Cold-start reset + onboarding prelude ---
|
||||
- runScript:
|
||||
file: ../scripts/reset_phone.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- runFlow: ../subflows/onboarding_returning_user.yaml
|
||||
|
||||
# --- Reset every mitra online first (so seed has someone to pick after
|
||||
# earlier offline-forcing tests left state dirty). ---
|
||||
- runScript:
|
||||
file: ../scripts/reset_all_mitras_online.js
|
||||
env:
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
# --- Seed history with M1, ensure a different M2 is online (the eventual
|
||||
# blast acceptor), then force M1 offline. ---
|
||||
- runScript:
|
||||
file: ../scripts/seed_history_session.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- runScript:
|
||||
file: ../scripts/force_other_mitra_online.js
|
||||
env:
|
||||
MITRA_ID: ${output.MITRA_ID}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- runScript:
|
||||
file: ../scripts/force_mitra_offline.js
|
||||
env:
|
||||
MITRA_ID: ${output.MITRA_ID}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
# Refresh home to pick up the seeded history row + offline status.
|
||||
- swipe:
|
||||
start: "50%, 30%"
|
||||
end: "50%, 80%"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*curhatan sebelumnya.*"
|
||||
timeout: 10000
|
||||
|
||||
# --- Steps 1-2: tap CTA → choice sheet → "bestie yang udah kenal" ---
|
||||
- tapOn:
|
||||
text: "(?s).*curhat sama bestie baru.*"
|
||||
retryTapIfNoChange: true
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*mau curhat sama siapa.*"
|
||||
timeout: 5000
|
||||
- tapOn: "(?s).*bestie yang udah kenal.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*bestie kamu sebelumnya.*"
|
||||
timeout: 5000
|
||||
|
||||
# --- Step 3: tap the DIMMED M1 row → BestieOfflinePopup (prePayReturning)
|
||||
# Popup title: "<mitraName> lagi nggak online" (verified against
|
||||
# bestie_unavailable_dialog.dart). CTA labels: "cari bestie lain" + "tanya
|
||||
# admin" + "kembali ke home" (ghost). ---
|
||||
- tapOn: "(?s).*bestie ${output.MITRA_NAME_RE}.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*${output.MITRA_NAME_RE}.*lagi nggak online.*"
|
||||
timeout: 10000
|
||||
- assertVisible: "(?s).*cari bestie lain.*"
|
||||
- assertVisible: "(?s).*tanya admin.*"
|
||||
|
||||
# --- Step 4: tap "cari bestie lain" → draft.reset() → /payment/entry ---
|
||||
- tapOn: "(?s).*cari bestie lain.*"
|
||||
|
||||
# /payment/entry forwards to /payment/method-pick for an ineligible
|
||||
# customer (prior completed session → no first-session discount).
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*pilih cara curhat.*"
|
||||
timeout: 10000
|
||||
|
||||
# --- Step 5: pick chat → duration → tier → cara bayar → bayar ---
|
||||
- tapOn:
|
||||
text: "(?s).*tulis dan baca dengan tenang.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*pilih durasi.*"
|
||||
timeout: 10000
|
||||
- tapOn: "(?s).*5 menit.*"
|
||||
- tapOn: "(?s).*bayar Rp.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*cara bayar.*"
|
||||
timeout: 10000
|
||||
- tapOn:
|
||||
text: "(?s).*bayar Rp.*"
|
||||
retryTapIfNoChange: true
|
||||
|
||||
# --- Step 6: waiting-payment → force-confirm ---
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "scan QRIS untuk bayar"
|
||||
timeout: 10000
|
||||
- runScript:
|
||||
file: ../scripts/mark_latest_payment_paid.js
|
||||
env:
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
# --- Step 7: notif-gate → /chat/searching (BLAST, not targeted-wait) ---
|
||||
# Because the popup's "cari bestie lain" CTA reset the draft, targetedMitraId
|
||||
# is null → searching_screen.dart fires startSearch() (blast), NOT
|
||||
# startTargetedSearch().
|
||||
- runFlow:
|
||||
when:
|
||||
visible:
|
||||
text: "(?s).*biar nggak ketinggalan.*"
|
||||
commands:
|
||||
- tapOn:
|
||||
text: "(?s).*nanti aja.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*lagi nyari bestie.*"
|
||||
timeout: 20000
|
||||
|
||||
# --- Step 8: any other online mitra accepts the blast ---
|
||||
# accept-latest-pending picks whichever mitra received the latest blast
|
||||
# notification — we don't care which one as long as it's not the seeded
|
||||
# (force-offline) one. Pre-req: ≥1 OTHER online mitra in the dev DB.
|
||||
- runScript:
|
||||
file: ../scripts/accept_latest_pending.js
|
||||
env:
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
# --- Step 9: chat screen renders ---
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*online.*"
|
||||
timeout: 20000
|
||||
@@ -0,0 +1,104 @@
|
||||
# TS-03 — Returning user picks offline bestie → "tanya admin" (escape)
|
||||
# (requirement/phase4-customer-flow.md → Test Scenarios → TS-03).
|
||||
#
|
||||
# §4 branch covered: Choice → "bestie yang udah kenal" → CheckOnline(no) →
|
||||
# OfflinePopup(pre-pay) → "tanya admin" → AdminSheet (terminal).
|
||||
#
|
||||
# This is a terminal-escape scenario: no payment row, no chat. The flow
|
||||
# ends after asserting the admin sheet renders with at least one contact
|
||||
# option (WhatsApp / Telegram).
|
||||
#
|
||||
# Pre-reqs (HARD):
|
||||
# - Backend reachable; NODE_ENV != 'production'.
|
||||
# - >= 1 mitra online in dev DB so `seed_history_session.js` can pick
|
||||
# someone to be the (about-to-be-forced-offline) M1. No second mitra
|
||||
# needed (no blast or accept in this scenario).
|
||||
# - `support_handles_json` in app_config has at least WA or Telegram
|
||||
# populated (default seed includes both). If empty, the admin sheet
|
||||
# renders "kontak admin belum tersedia" — the assertion will still pass
|
||||
# on the "tanya admin" title but the deeplink CTA assertion below would
|
||||
# fail; in that case the WA/Telegram assertion is the verification of
|
||||
# interest.
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/ts-03_returning_lama_offline_tanya_admin.yaml
|
||||
appId: com.halobestie.client.client_app
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
---
|
||||
# --- Cold-start reset + onboarding prelude ---
|
||||
- runScript:
|
||||
file: ../scripts/reset_phone.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- runFlow: ../subflows/onboarding_returning_user.yaml
|
||||
|
||||
# --- Seed history with M1 then force M1 offline ---
|
||||
- runScript:
|
||||
file: ../scripts/seed_history_session.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- runScript:
|
||||
file: ../scripts/force_mitra_offline.js
|
||||
env:
|
||||
MITRA_ID: ${output.MITRA_ID}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
# Refresh home so history list shows the dimmed row.
|
||||
- swipe:
|
||||
start: "50%, 30%"
|
||||
end: "50%, 80%"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*curhatan sebelumnya.*"
|
||||
timeout: 10000
|
||||
|
||||
# --- Steps 1-2: choice sheet → "bestie yang udah kenal" → history list ---
|
||||
- tapOn:
|
||||
text: "(?s).*curhat sama bestie baru.*"
|
||||
retryTapIfNoChange: true
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*mau curhat sama siapa.*"
|
||||
timeout: 5000
|
||||
- tapOn: "(?s).*bestie yang udah kenal.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*bestie kamu sebelumnya.*"
|
||||
timeout: 5000
|
||||
|
||||
# --- Step 3: tap dimmed M1 row → BestieOfflinePopup (prePayReturning) ---
|
||||
- tapOn: "(?s).*bestie ${output.MITRA_NAME_RE}.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*${output.MITRA_NAME_RE}.*lagi nggak online.*"
|
||||
timeout: 10000
|
||||
- assertVisible: "(?s).*cari bestie lain.*"
|
||||
- assertVisible: "(?s).*tanya admin.*"
|
||||
|
||||
# --- Step 4: tap "tanya admin" → admin sheet opens ON TOP of the popup
|
||||
# (per bestie_unavailable_dialog.dart L181-187 the popup stays alive under
|
||||
# the sheet). The sheet title is "tanya admin". ---
|
||||
- tapOn: "(?s).*tanya admin.*"
|
||||
|
||||
# The admin sheet's title is "tanya admin" — but so was the CTA we just
|
||||
# tapped, which remains rendered behind the sheet. Disambiguate by also
|
||||
# asserting on the subtitle copy "pilih cara yang paling enak buat kamu",
|
||||
# which only exists on the sheet (verified against tanya_admin_sheet.dart).
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*pilih cara yang paling enak buat kamu.*"
|
||||
timeout: 10000
|
||||
- assertVisible: "(?s).*tanya admin.*"
|
||||
|
||||
# Confirm at least one contact option renders. If support_handles_json is
|
||||
# empty in app_config the sheet renders "kontak admin belum tersedia" — in
|
||||
# that case the assertVisible above (title + subtitle) is the meaningful
|
||||
# check; the WhatsApp/Telegram presence depends on CC seed.
|
||||
# TODO(test-data): if dev seeds reliably populate WA + Telegram, tighten
|
||||
# this to assertVisible "WhatsApp|Telegram".
|
||||
134
client_app/.maestro/flows/ts-04_returning_baru_blast.yaml
Normal file
134
client_app/.maestro/flows/ts-04_returning_baru_blast.yaml
Normal file
@@ -0,0 +1,134 @@
|
||||
# TS-04 — Returning user picks "bestie baru" → blast happy path
|
||||
# (requirement/phase4-customer-flow.md → Test Scenarios → TS-04).
|
||||
#
|
||||
# §4 branch covered: Choice → "bestie baru" → PickMethod → PickDuration →
|
||||
# PayMethod → Bayar → WaitPay → paid → PairRoute(baru) → BlastFlow →
|
||||
# S10 chat.
|
||||
#
|
||||
# What this proves: tapping "bestie baru" explicitly resets the payment
|
||||
# draft (clearing any stale targetedMitraId — Stage 5.1 Risk #4
|
||||
# mitigation), so the post-payment route is /chat/searching (blast), NOT
|
||||
# /chat/waiting-targeted/...
|
||||
#
|
||||
# Pre-reqs (HARD):
|
||||
# - Backend reachable; NODE_ENV != 'production'.
|
||||
# - At least ONE mitra is online to accept the blast.
|
||||
# - The customer must qualify as "returning" so the choice sheet renders
|
||||
# (not auto-jumps to /payment/entry). We seed a history row to force
|
||||
# this. The seeded mitra does NOT need to stay online — they're not
|
||||
# the acceptor here (this is the "bestie baru" branch — blast goes to
|
||||
# whoever's online).
|
||||
# - Dev seed (backend/src/db/seed.js) does NOT auto-create mitras — sign
|
||||
# in at least one test mitra via mitra_app + heartbeat.
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/ts-04_returning_baru_blast.yaml
|
||||
appId: com.halobestie.client.client_app
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
---
|
||||
# --- Cold-start reset + onboarding prelude ---
|
||||
- runScript:
|
||||
file: ../scripts/reset_phone.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- runFlow: ../subflows/onboarding_returning_user.yaml
|
||||
|
||||
# --- Seed history so the customer qualifies as "returning" and the choice
|
||||
# sheet (with both options) renders. The seeded mitra is who'll later
|
||||
# accept the blast — they just need to stay online. ---
|
||||
- runScript:
|
||||
file: ../scripts/seed_history_session.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
- swipe:
|
||||
start: "50%, 30%"
|
||||
end: "50%, 80%"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*curhatan sebelumnya.*"
|
||||
timeout: 10000
|
||||
|
||||
# --- Steps 1-2: tap CTA → choice sheet → "bestie baru" ---
|
||||
- tapOn:
|
||||
text: "(?s).*curhat sama bestie baru.*"
|
||||
retryTapIfNoChange: true
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*mau curhat sama siapa.*"
|
||||
timeout: 5000
|
||||
|
||||
# "bestie baru" branch — onTap path resets the draft + pushes
|
||||
# /payment/entry. The label "bestie baru" alone is too short and may also
|
||||
# match the CTA we just tapped on home; use the subtitle text "cari bestie
|
||||
# baru yang siap dengerin" which is unique to this card.
|
||||
- tapOn: "(?s).*cari bestie baru yang siap dengerin.*"
|
||||
|
||||
# /payment/entry → /payment/method-pick (returning customer, no discount).
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*pilih cara curhat.*"
|
||||
timeout: 10000
|
||||
|
||||
# --- Step 3: pick chat → duration → tier → cara bayar → bayar ---
|
||||
- tapOn:
|
||||
text: "(?s).*tulis dan baca dengan tenang.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*pilih durasi.*"
|
||||
timeout: 10000
|
||||
- tapOn: "(?s).*5 menit.*"
|
||||
- tapOn: "(?s).*bayar Rp.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*cara bayar.*"
|
||||
timeout: 10000
|
||||
- tapOn:
|
||||
text: "(?s).*bayar Rp.*"
|
||||
retryTapIfNoChange: true
|
||||
|
||||
# --- Step 4: waiting-payment → force-confirm ---
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "scan QRIS untuk bayar"
|
||||
timeout: 10000
|
||||
- runScript:
|
||||
file: ../scripts/mark_latest_payment_paid.js
|
||||
env:
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
# --- Step 5: notif-gate → /chat/searching (BLAST, not targeted-wait) ---
|
||||
- runFlow:
|
||||
when:
|
||||
visible:
|
||||
text: "(?s).*biar nggak ketinggalan.*"
|
||||
commands:
|
||||
- tapOn:
|
||||
text: "(?s).*nanti aja.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*lagi nyari bestie.*"
|
||||
timeout: 20000
|
||||
|
||||
# --- Step 6: any online mitra accepts the blast ---
|
||||
# We use the seeded mitra's id as a known-online acceptor. (If they're no
|
||||
# longer online, the assertion below will time out — re-run after
|
||||
# refreshing their heartbeat.)
|
||||
- runScript:
|
||||
file: ../scripts/mitra_accept_latest_internal.js
|
||||
env:
|
||||
MITRA_ID: ${output.MITRA_ID}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
# --- Step 7: chat screen renders ---
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*online.*"
|
||||
timeout: 20000
|
||||
- assertVisible: "(?s).*${output.MITRA_NAME_RE}.*"
|
||||
@@ -0,0 +1,143 @@
|
||||
# TS-05 — Payment expired → retry preserves targeting
|
||||
# (requirement/phase4-customer-flow.md → Test Scenarios → TS-05).
|
||||
#
|
||||
# §4 branch covered: PickMethod → PickDuration → PayMethod → WaitPay →
|
||||
# PayStat(timeout 20 min) → PayExpired → Pay(retry) → paid →
|
||||
# PairRoute(lama) → Targeted → S10.
|
||||
#
|
||||
# What this proves: the `targetedMitraId` on the payment draft survives
|
||||
# the expired-retry round trip (Stage 5.1 `resetExceptTarget` invariant).
|
||||
# After the retry pays, the customer lands on /chat/waiting-targeted/<SAME
|
||||
# mitraId> — NOT a fresh blast.
|
||||
#
|
||||
# Pre-reqs (HARD):
|
||||
# - Backend reachable; NODE_ENV != 'production'.
|
||||
# - >= 1 mitra online — they're the targeted mitra throughout. The flow
|
||||
# does NOT drive accept (we stop at the targeted-waiting screen — the
|
||||
# assertion that targeting survived the retry is the goal).
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/ts-05_payment_expired_retry_preserves_targeting.yaml
|
||||
appId: com.halobestie.client.client_app
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
---
|
||||
# --- Cold-start reset + onboarding prelude ---
|
||||
- runScript:
|
||||
file: ../scripts/reset_phone.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- runFlow: ../subflows/onboarding_returning_user.yaml
|
||||
|
||||
# --- Seed history with online M1 (targeted throughout). ---
|
||||
- runScript:
|
||||
file: ../scripts/seed_history_session.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
- swipe:
|
||||
start: "50%, 30%"
|
||||
end: "50%, 80%"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*curhatan sebelumnya.*"
|
||||
timeout: 10000
|
||||
|
||||
# --- Walk TS-01 steps 1-6 to reach /payment/waiting for the targeted
|
||||
# attempt against M1. ---
|
||||
- tapOn:
|
||||
text: "(?s).*curhat sama bestie baru.*"
|
||||
retryTapIfNoChange: true
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*mau curhat sama siapa.*"
|
||||
timeout: 5000
|
||||
- tapOn: "(?s).*bestie yang udah kenal.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*bestie kamu sebelumnya.*"
|
||||
timeout: 5000
|
||||
- tapOn: "(?s).*bestie ${output.MITRA_NAME_RE}.*"
|
||||
|
||||
# /payment/entry → /payment/method-pick (returning, no discount).
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*pilih cara curhat.*"
|
||||
timeout: 10000
|
||||
- tapOn:
|
||||
text: "(?s).*tulis dan baca dengan tenang.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*pilih durasi.*"
|
||||
timeout: 10000
|
||||
- tapOn: "(?s).*5 menit.*"
|
||||
- tapOn: "(?s).*bayar Rp.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*cara bayar.*"
|
||||
timeout: 10000
|
||||
- tapOn:
|
||||
text: "(?s).*bayar Rp.*"
|
||||
retryTapIfNoChange: true
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "scan QRIS untuk bayar"
|
||||
timeout: 10000
|
||||
|
||||
# --- Force-expire the latest pending payment ---
|
||||
- runScript:
|
||||
file: ../scripts/force_expire_latest_payment.js
|
||||
env:
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
# --- Poller picks `expired` within ~3s → /payment/expired/:paymentId ---
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*pembayaran kedaluwarsa.*"
|
||||
timeout: 10000
|
||||
- assertVisible: "(?s).*coba lagi.*"
|
||||
|
||||
# --- Tap retry → /payment/method (NOT /payment/method-pick — the draft
|
||||
# was preserved via resetExceptTarget, so we skip mode + duration). ---
|
||||
- tapOn: "(?s).*coba lagi.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*cara bayar.*"
|
||||
timeout: 10000
|
||||
# Sanity check: we did NOT bounce back to the mode picker.
|
||||
- assertNotVisible: "(?s).*pilih cara curhat.*"
|
||||
|
||||
# --- Re-pay ---
|
||||
- tapOn:
|
||||
text: "(?s).*bayar Rp.*"
|
||||
retryTapIfNoChange: true
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "scan QRIS untuk bayar"
|
||||
timeout: 10000
|
||||
- runScript:
|
||||
file: ../scripts/mark_latest_payment_paid.js
|
||||
env:
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
# --- Notif-gate → targeted-waiting screen for the SAME mitra (M1).
|
||||
# This is the load-bearing assertion: if targetedMitraId had been wiped
|
||||
# by the expired-retry round trip, the customer would land on
|
||||
# /chat/searching (blast) and we'd see "lagi nyari bestie" instead. ---
|
||||
- runFlow:
|
||||
when:
|
||||
visible:
|
||||
text: "(?s).*biar nggak ketinggalan.*"
|
||||
commands:
|
||||
- tapOn:
|
||||
text: "(?s).*nanti aja.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*MENUNGGU JAWABAN.*"
|
||||
timeout: 20000
|
||||
- assertVisible: "(?s).*lagi nungguin ${output.MITRA_NAME_RE}.*"
|
||||
@@ -0,0 +1,187 @@
|
||||
# TS-06 — Targeted request fails post-payment → fallback to blast
|
||||
# (requirement/phase4-customer-flow.md → Test Scenarios → TS-06).
|
||||
#
|
||||
# §4 branch covered: Targeted → TargetedRes(reject/timeout) →
|
||||
# OfflinePopup(post-pay, returning variant) → "cari bestie lain" →
|
||||
# fallback-to-blast → §3 BlastFlow → S10.
|
||||
#
|
||||
# What this proves: after paying for a targeted attempt, if the picked
|
||||
# mitra rejects or times out, the customer can fall back to general blast
|
||||
# WITHOUT a second payment (same payment_sessions row reused).
|
||||
#
|
||||
# Pre-reqs (HARD):
|
||||
# - Backend reachable; NODE_ENV != 'production'.
|
||||
# - >= 2 mitras online in dev DB:
|
||||
# 1. M1 — picked from history, becomes the targeted mitra; we
|
||||
# simulate them rejecting via force_pairing_timeout.js.
|
||||
# 2. M2 — the blast-fallback acceptor.
|
||||
# If only one mitra is online, the fallback-to-blast cannot match.
|
||||
#
|
||||
# Known backend gap (TODO):
|
||||
# The current force_pairing_timeout.js endpoint calls
|
||||
# `expirePairingRequest()` which broadcasts WS PAIRING_FAILED
|
||||
# (is_terminal=false). On the TargetedWaitingScreen, this maps the
|
||||
# pairing state to PairingFailedData, NOT PairingTargetedUnavailableData
|
||||
# — so the `BestieOfflinePopup` (returning variant) won't fire from the
|
||||
# targeted-waiting screen as written.
|
||||
#
|
||||
# To make this flow pass end-to-end the backend test endpoint needs to
|
||||
# detect when the latest pending_acceptance session is a TARGETED pair
|
||||
# (chat_sessions.targeted_mitra_id is set / linked to a confirmed
|
||||
# payment_session.targeted_mitra_id) and route to
|
||||
# `expireTargetedPairingRequest` instead, which broadcasts
|
||||
# RETURNING_CHAT_TIMEOUT → PairingTargetedUnavailableData → popup fires.
|
||||
#
|
||||
# Until that fix lands, this flow will fail at step "OfflinePopup
|
||||
# visible". Leave it in place: it correctly expresses the intended
|
||||
# product behavior and serves as a regression test for the backend fix.
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/ts-06_targeted_reject_fallback_to_blast.yaml
|
||||
appId: com.halobestie.client.client_app
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
# Second online mitra that will accept the blast fallback. See task spec
|
||||
# "Open question" — TS-06 needs >= 2 online mitras and no "any online
|
||||
# acceptor" helper exists yet.
|
||||
TEST_MITRA_ID_ACCEPTOR: "${TEST_MITRA_ID_ACCEPTOR}"
|
||||
---
|
||||
# --- Cold-start reset + onboarding prelude ---
|
||||
- runScript:
|
||||
file: ../scripts/reset_phone.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- runFlow: ../subflows/onboarding_returning_user.yaml
|
||||
|
||||
# --- Reset every mitra online first (test idempotency). ---
|
||||
- runScript:
|
||||
file: ../scripts/reset_all_mitras_online.js
|
||||
env:
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
# --- Seed history with M1 (online — they'll be the targeted mitra). Then
|
||||
# ensure a different M2 is online so the post-rejection blast has an
|
||||
# acceptor. ---
|
||||
- runScript:
|
||||
file: ../scripts/seed_history_session.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- runScript:
|
||||
file: ../scripts/force_other_mitra_online.js
|
||||
env:
|
||||
MITRA_ID: ${output.MITRA_ID}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
- swipe:
|
||||
start: "50%, 30%"
|
||||
end: "50%, 80%"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*curhatan sebelumnya.*"
|
||||
timeout: 10000
|
||||
|
||||
# --- Walk TS-01 steps 1-8 to reach /chat/waiting-targeted/<mitraId> ---
|
||||
- tapOn:
|
||||
text: "(?s).*curhat sama bestie baru.*"
|
||||
retryTapIfNoChange: true
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*mau curhat sama siapa.*"
|
||||
timeout: 5000
|
||||
- tapOn: "(?s).*bestie yang udah kenal.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*bestie kamu sebelumnya.*"
|
||||
timeout: 5000
|
||||
- tapOn: "(?s).*bestie ${output.MITRA_NAME_RE}.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*pilih cara curhat.*"
|
||||
timeout: 10000
|
||||
- tapOn:
|
||||
text: "(?s).*tulis dan baca dengan tenang.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*pilih durasi.*"
|
||||
timeout: 10000
|
||||
- tapOn: "(?s).*5 menit.*"
|
||||
- tapOn: "(?s).*bayar Rp.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*cara bayar.*"
|
||||
timeout: 10000
|
||||
- tapOn:
|
||||
text: "(?s).*bayar Rp.*"
|
||||
retryTapIfNoChange: true
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "scan QRIS untuk bayar"
|
||||
timeout: 10000
|
||||
- runScript:
|
||||
file: ../scripts/mark_latest_payment_paid.js
|
||||
env:
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- runFlow:
|
||||
when:
|
||||
visible:
|
||||
text: "(?s).*biar nggak ketinggalan.*"
|
||||
commands:
|
||||
- tapOn:
|
||||
text: "(?s).*nanti aja.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*MENUNGGU JAWABAN.*"
|
||||
timeout: 20000
|
||||
|
||||
# --- Force the targeted pending_acceptance session to expire ---
|
||||
# TODO(backend): force-pairing-timeout currently calls
|
||||
# expirePairingRequest (broadcasts PAIRING_FAILED), not
|
||||
# expireTargetedPairingRequest (which would broadcast
|
||||
# RETURNING_CHAT_TIMEOUT). The popup assertion below depends on the
|
||||
# RETURNING_CHAT_TIMEOUT path — see header note.
|
||||
- runScript:
|
||||
file: ../scripts/force_pairing_timeout.js
|
||||
env:
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
# --- BestieOfflinePopup (returning variant, post-pay) appears ---
|
||||
# Title from bestie_unavailable_dialog.dart for the `returning` variant:
|
||||
# "<mitraName> lagi nggak online". Primary CTA when canFallbackToBlast is
|
||||
# true (M2 is reachable + paymentSessionId is set): "chat dengan bestie
|
||||
# lain". This differs from the prePayReturning variant's "cari bestie
|
||||
# lain" CTA — verified in bestie_unavailable_dialog.dart L140-152.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*${output.MITRA_NAME_RE}.*lagi nggak online.*"
|
||||
timeout: 15000
|
||||
- assertVisible: "(?s).*chat dengan bestie lain.*"
|
||||
|
||||
# --- Tap "chat dengan bestie lain" → fallbackToBlast() ---
|
||||
# In current code, fallback-to-blast creates a fresh pending request that
|
||||
# may render as either /chat/searching ("lagi nyari bestie") for a
|
||||
# multi-mitra blast OR /chat/waiting-targeted (MENUNGGU JAWABAN) when the
|
||||
# backend reuses the existing payment session to target the next available
|
||||
# mitra. Either pending state is acceptable here — the critical assertion
|
||||
# is that the customer wasn't charged again (same payment_sessions row).
|
||||
- tapOn: "(?s).*chat dengan bestie lain.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*(lagi nyari bestie|MENUNGGU JAWABAN).*"
|
||||
timeout: 15000
|
||||
|
||||
# --- M2 accepts the blast (any other online mitra) ---
|
||||
- runScript:
|
||||
file: ../scripts/accept_latest_pending.js
|
||||
env:
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
# --- Chat screen renders (with M2, not M1) ---
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*online.*"
|
||||
timeout: 20000
|
||||
17
client_app/.maestro/scripts/accept_latest_pending.js
Normal file
17
client_app/.maestro/scripts/accept_latest_pending.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// Accept the latest pending pairing notification regardless of mitra. Used
|
||||
// by flows where the acceptor mitra UUID isn't known in advance — e.g.
|
||||
// TS-02 (blast where the seeded mitra was forced offline, so an unknown
|
||||
// OTHER online mitra got the chat_request_notification row).
|
||||
//
|
||||
// Backed by /internal/_test/accept-latest-pending (no body needed).
|
||||
const url = BACKEND_INTERNAL_URL || 'http://localhost:3001'
|
||||
const resp = http.post(`${url}/internal/_test/accept-latest-pending`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: '{}',
|
||||
})
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(`accept-latest-pending failed (${resp.status}): ${resp.body}`)
|
||||
}
|
||||
const data = json(resp.body)
|
||||
output.ACCEPTED_SESSION_ID = data.session_id
|
||||
output.ACCEPTOR_MITRA_ID = data.mitra_id
|
||||
20
client_app/.maestro/scripts/force_mitra_offline.js
Normal file
20
client_app/.maestro/scripts/force_mitra_offline.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// Force a specific mitra OFFLINE via the dev-only
|
||||
// /internal/_test/force-mitra-offline endpoint. Used by Maestro flows that
|
||||
// need the bestie-history-list row for a particular mitra to render in its
|
||||
// offline (dimmed) state — see TS-02 / TS-03 in
|
||||
// requirement/phase4-customer-flow.md.
|
||||
//
|
||||
// Reads MITRA_ID from env (typically `${output.MITRA_ID}` from a prior
|
||||
// seed_history_session.js run) and BACKEND_INTERNAL_URL.
|
||||
const mitraId = MITRA_ID
|
||||
const url = BACKEND_INTERNAL_URL || 'http://localhost:3001'
|
||||
if (!mitraId) {
|
||||
throw new Error('MITRA_ID env not set — pass output.MITRA_ID from seed_history_session.js')
|
||||
}
|
||||
const resp = http.post(`${url}/internal/_test/force-mitra-offline`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mitra_id: mitraId }),
|
||||
})
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(`force-mitra-offline failed (${resp.status}): ${resp.body}`)
|
||||
}
|
||||
17
client_app/.maestro/scripts/force_other_mitra_online.js
Normal file
17
client_app/.maestro/scripts/force_other_mitra_online.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// Force a DIFFERENT mitra online (one other than the seeded one) so blast
|
||||
// flows (TS-02, TS-06) have an acceptor available after the seeded mitra is
|
||||
// forced offline. Picks any currently-offline mitra excluding MITRA_ID.
|
||||
//
|
||||
// Reads MITRA_ID (the seeded mitra to exclude, from seed_history_session.js)
|
||||
// and BACKEND_INTERNAL_URL.
|
||||
const excludeId = MITRA_ID
|
||||
const url = BACKEND_INTERNAL_URL || 'http://localhost:3001'
|
||||
const resp = http.post(`${url}/internal/_test/force-mitra-online`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ exclude_mitra_id: excludeId }),
|
||||
})
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(`force-mitra-online failed (${resp.status}): ${resp.body}`)
|
||||
}
|
||||
const data = json(resp.body)
|
||||
output.OTHER_MITRA_ID = data.mitra_id
|
||||
19
client_app/.maestro/scripts/mitra_accept_latest_internal.js
Normal file
19
client_app/.maestro/scripts/mitra_accept_latest_internal.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// Have the test mitra "accept" the most recent pending pairing request via
|
||||
// the dev-only /internal/_test/mitra-accept-latest endpoint (no JWT needed).
|
||||
//
|
||||
// Reads MITRA_ID from the env that the calling flow injects — typically
|
||||
// `${output.MITRA_ID}` from a prior seed_history_session.js run.
|
||||
const mitraId = MITRA_ID
|
||||
const url = BACKEND_INTERNAL_URL || 'http://localhost:3001'
|
||||
if (!mitraId) {
|
||||
throw new Error('MITRA_ID env not set — pass output.MITRA_ID from seed_history_session.js')
|
||||
}
|
||||
const resp = http.post(`${url}/internal/_test/mitra-accept-latest`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mitra_id: mitraId }),
|
||||
})
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(`mitra-accept-latest failed (${resp.status}): ${resp.body}`)
|
||||
}
|
||||
const data = json(resp.body)
|
||||
output.ACCEPTED_SESSION_ID = data.session_id
|
||||
16
client_app/.maestro/scripts/reset_all_mitras_online.js
Normal file
16
client_app/.maestro/scripts/reset_all_mitras_online.js
Normal file
@@ -0,0 +1,16 @@
|
||||
// Bulk-mark every mitra row online in mitra_online_status. Used as a setup
|
||||
// step at the start of each Maestro flow so seed_history_session has at
|
||||
// least one online mitra to pick, regardless of what previous tests did
|
||||
// (e.g. force-mitra-offline lingering from a prior TS-02/TS-03 run).
|
||||
//
|
||||
// Backed by /internal/_test/reset-all-mitras-online.
|
||||
const url = BACKEND_INTERNAL_URL || 'http://localhost:3001'
|
||||
const resp = http.post(`${url}/internal/_test/reset-all-mitras-online`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: '{}',
|
||||
})
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(`reset-all-mitras-online failed (${resp.status}): ${resp.body}`)
|
||||
}
|
||||
const data = json(resp.body)
|
||||
output.ONLINE_COUNT = data.online_count
|
||||
@@ -16,3 +16,7 @@ const data = json(resp.body)
|
||||
output.SESSION_ID = data.session_id
|
||||
output.MITRA_ID = data.mitra_id
|
||||
output.MITRA_NAME = data.mitra_name
|
||||
// Regex-escaped variant for Maestro `text:` selectors (which do FULL-string
|
||||
// regex match). Display names can contain `+` (phone-as-name), `.`, etc.
|
||||
// which break selectors otherwise.
|
||||
output.MITRA_NAME_RE = data.mitra_name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
|
||||
100
client_app/.maestro/subflows/onboarding_returning_user.yaml
Normal file
100
client_app/.maestro/subflows/onboarding_returning_user.yaml
Normal file
@@ -0,0 +1,100 @@
|
||||
# Shared onboarding prelude for Phase 4 §4 "returning user" Maestro flows
|
||||
# (TS-01 through TS-06 — see requirement/phase4-customer-flow.md).
|
||||
#
|
||||
# This subflow drives a clean-slate emulator from cold start to /home as a
|
||||
# verified customer. The verified+display-name'd state is the precondition
|
||||
# for every TS scenario in §4, so we extract it here to avoid ~80 lines of
|
||||
# duplication across the six flows.
|
||||
#
|
||||
# Pre-reqs (parent flow's responsibility):
|
||||
# - Parent flow has `env:` block defining TEST_PHONE and
|
||||
# BACKEND_INTERNAL_URL (Maestro subflows inherit env from caller).
|
||||
# - Parent flow runs `reset_phone.js` + `launchApp clearState: true`
|
||||
# BEFORE invoking this subflow.
|
||||
# - NODE_ENV != 'production' on backend (so /internal/_test routes exist).
|
||||
#
|
||||
# Path taken:
|
||||
# Welcome carousel ("Mulai") → Home with anon banner →
|
||||
# tap "masuk →" → /auth/register → enter +62 subscriber digits →
|
||||
# "kirim kode" → OTP screen → peek OTP from stub → auto-submit →
|
||||
# /auth/set-name (because AuthNeedsDisplayNameData) → enter "Maestro" →
|
||||
# "Lanjut" → /home (returning view: "curhatan sebelumnya" header).
|
||||
#
|
||||
# Selector style: Flutter merges sibling Text widgets inside a single
|
||||
# tappable parent into ONE accessibility blob. Maestro's `text:` selector
|
||||
# does a FULL-string regex match, so we wrap selectors in `(?s).*…*` for
|
||||
# anything that lives inside an InkWell with multiple Texts. Empty
|
||||
# TextFields don't expose their hint to Maestro a11y, so we tap by point
|
||||
# inside the field's pill before typing.
|
||||
appId: ${APP_ID_ANDROID}
|
||||
---
|
||||
# Welcome carousel — the "Mulai" button is the only Text inside its
|
||||
# tappable region, so a plain selector works.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "Mulai"
|
||||
timeout: 15000
|
||||
- tapOn:
|
||||
text: "Mulai"
|
||||
retryTapIfNoChange: true
|
||||
|
||||
# Home — login-recover banner ("udah pernah pakai HaloBestie? … masuk →")
|
||||
# is one InkWell, so any token within its merged blob reaches the
|
||||
# /auth/register handler.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*udah pernah pakai HaloBestie.*"
|
||||
timeout: 30000
|
||||
- tapOn:
|
||||
text: "(?s).*masuk →.*"
|
||||
|
||||
# Register screen — personalised title "nomor wa-mu, {name}?" is in a
|
||||
# merged blob with the subtitle copy.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*nomor wa-mu.*"
|
||||
timeout: 10000
|
||||
|
||||
# Phone input — +62 is a static prefix chip; type only subscriber digits
|
||||
# (no leading 0). +6281234567890 → 81234567890. Tap by point inside the
|
||||
# pill (well right of the +62 chip, well above the kirim-kode button).
|
||||
- tapOn:
|
||||
point: "60%, 47%"
|
||||
- inputText: "81234567890"
|
||||
- hideKeyboard
|
||||
- tapOn:
|
||||
text: "(?s).*kirim kode.*"
|
||||
|
||||
# OTP screen — peek the code from the stub endpoint.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "Masukkan OTP"
|
||||
timeout: 15000
|
||||
- runScript:
|
||||
file: ../scripts/peek_otp.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- inputText: ${output.OTP}
|
||||
|
||||
# After 6th digit OTP auto-submits. The anon customer's phone is attached
|
||||
# but display_name is still empty → AuthNeedsDisplayNameData → router
|
||||
# pushes /auth/set-name ("Siapa namamu?").
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Siapa namamu.*"
|
||||
timeout: 20000
|
||||
# Same TextField-hint-invisible-to-Maestro issue as the phone field — tap
|
||||
# by point inside the "Nama panggilan" pill, then type a display name.
|
||||
- tapOn:
|
||||
point: "50%, 30%"
|
||||
- inputText: "Maestro"
|
||||
- hideKeyboard
|
||||
- tapOn: "Lanjut"
|
||||
|
||||
# Now home renders the returning view ("curhatan sebelumnya" section
|
||||
# header is the deterministic landmark — appears regardless of history).
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*curhatan sebelumnya.*"
|
||||
timeout: 30000
|
||||
Reference in New Issue
Block a user