Spec §2 (flow_customer.mermaid) routes post-OTP based on user-lookup + has_transacted, but the implementation previously dumped every OTP success on /home. Introduce `OnboardingIntent` provider: set to `onboarding` by routeForVerifChoice's verified branch (the "aku mau curhat" transaction journey), set to `recover` by SHome1st's masuk → banner. Router redirect on AuthAuthenticatedData+isAuthRoute consumes it: `onboarding` → /payment/entry (dispatches S6 paywall vs PickMethod via first_session_discount.eligible); `recover` → /home. Intent is reset in /payment/entry's initState so subsequent masuk → flows don't inherit it. auth_notifier.verifyOtp uses .copyWithPrevious on AsyncError so valueOrNull retains AuthOtpSentData/AuthAnonymousData through OTP failures — required for the OTP-blocked recovery path (/onboarding/anon/method → /payment/method-pick) to clear the global redirect without bouncing to /home. Router also extends the isAuthRoute/isOnboardingFlow carve-out to AuthOtpSentData. Maestro tests adopt `ts-<app>-<NN>-<MM>-<descriptor>.yaml` convention: NN = mermaid section, MM = sub-flow index. New ts-customer-02-01..05 cover the §2 branches (verified brand-new → S6, existing-no-tx → S6, existing-tx → method-pick, OTP-blocked → method-pick, anonymous first- timer → method-pick); deferred 02-06/07/08/09 documented in README_section_02.md. TS-07 → ts-customer-02-10 (masuk → recovery); TS-01..06 → ts-customer-04-01..06 (§4 returning-user). Shared onboarding_new_user_verified.yaml subflow extracted. Register screen's body Column now uses LayoutBuilder + SingleChildScrollView + ConstrainedBox + IntrinsicHeight so the keyboard-open layout no longer overflows by 1.3 px (verified visually). Spec prose updated at flow_customer.mermaid §2 to describe the intent-driven routing + login-vs-transaction divergence. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
188 lines
6.7 KiB
YAML
188 lines
6.7 KiB
YAML
# 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
|