§1 notif banner: permission_handler v11 returns granted unconditionally
for Permission.notification on Android <13 because POST_NOTIFICATIONS
didn't exist as a runtime permission. Result: SHome1st amber "notifikasi
off" banner never showed on API 24-32 even when the user toggled
notifications off in Settings → Apps. Add a
NotificationManagerCompat.areNotificationsEnabled() pre-check via
flutter_local_notifications (works from API 19+) so the banner reflects
the real OS state on older Android.
§5 chat delivery: the contract is "WS when foreground, FCM when
background", but the previous build only honoured (1) — Android keeps
the TCP socket alive after the Dart isolate is paused, so backend's
`socket.readyState === 1` check returned true and FCM never fired.
Fix has five parts (all required together):
1. Customer-side lifecycle observer in client_app/main.dart closes
chatProvider's WS on paused/detached, reconnects on resumed.
2. `_appPaused` gate in main.dart suppresses the activeSessionProvider
listener's auto-reconnect (15s poll in active_session_notifier
would otherwise re-open the WS the next tick after the observer
closed it — defeating the fallback).
3. Mitra-side lifecycle observer in mitra_app/main.dart stashes
`_pausedChatSessionId`, calls mitraChatProvider.disconnect(), and
re-issues connect(saved) on resumed.
4. MitraChat gains a `_connectedSessionId` field + getter so the
observer in step 3 can read it back across disconnect (disconnect
clears it; the next connect overwrites it).
5. SearchingScreen resets pairingProvider when entering with a new
draft.paymentId — previously it retained PairingActiveData with
the *old* sessionId after a session ended, and the next pairing
flow navigated straight to that completed session showing
"Sesi sudah berakhir".
Backend additions under /internal/_test/* for assertion harness:
inspectSessionWsState + GET /ws-connection-state,
POST /send-chat-message-as-mitra (with delivered_via),
POST /send-chat-message-as-customer (with delivered_via),
POST /send-fcm-chat-message (raw FCM dispatch).
Maestro coverage:
- ts-customer-05-01: mitra → customer message when customer is
backgrounded → delivered_via=fcm.
- ts-customer-05-02: customer → mitra message when mitra is
backgrounded → delivered_via=fcm.
- ts-customer-01-01: §1 notif-denied banner on home. Documented
precondition: mitra must be force-stopped or backgrounded on the
chat screen before 05-02 runs (Maestro can only drive one --udid
per run; mitra-side lifecycle observer end-to-end is deferred).
Helper scripts under client_app/.maestro/scripts/:
inspect_ws_state.js, assert_ws_state.js,
send_chat_message_as_mitra.js, assert_delivered_via.js (takes
SENDER=mitra|customer to route to the matching backend endpoint).
README_section_05.md documents the test plan, helper scripts, and the
deferred mitra-side maestro driving. Both apps tested manually on
API 28 AVDs where FCM delivery is sub-second; API 24 has 5-30 min
heartbeats that make it impractical for FCM-related testing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
client_app Maestro flows
End-to-end UI automation for the customer Flutter app using Maestro. 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:
curl -Ls "https://get.maestro.mobile.dev" | bash
Verify with maestro --version. See the Maestro install docs for Homebrew / chocolatey / Docker alternatives.
You also need:
adbon your PATH (comes with Android Studio's platform-tools)jqfor 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 withTEST_MITRA_IDandTEST_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):
# 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:
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:
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:
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:
- Maestro flow drives customer up to the "Mencari Bestie..." state
runScript: ../scripts/mitra_accept_latest.shfiresPOST /api/mitra/chat-requests/:id/acceptagainst the backend, using a pre-minted mitra JWT- 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. The 10% that needs both UIs running (e.g., asserting the mitra-side overlay countdown displays correctly) is in mitra_app/.maestro/ and runs separately.
Adding a new flow
Pick a Phase 3.7 testing checklist scenario (see phase3.7-testing.md), then:
- Copy an existing flow as a template (e.g.,
03_payment_to_chat_happy.yaml) - Update the pre-req comment, the steps, and the assertions
- If you need a "second actor" action, add a bash helper under
scripts/and call it viarunScript: - If you need new env vars, add them to
config.yamlwith sensible defaults
Tips
- Find the right text to tap on —
maestro studioopens 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.yamlsaves screenshots + logs per step. - Add flows incrementally — Maestro's reload-on-save in
maestro studiomakes iteration fast. - Don't commit screenshots / debug output — add
.maestro/output/and.maestro/screenshots/to.gitignoreif you generate them locally.
Troubleshooting
maestro: device not found→ runadb devices; if empty, start an emulator (emulator -avd <name>) or plug in a USB device with debugging enabled.Element not visibleerrors → usemaestro studioto inspect actual labels — they may have changed since the flow was written.- Flow hangs at
assertVisiblewaiting for backend → checkBACKEND_URLmatches the APK's build-time value (grep API_BASE_URL build.gradle). runScriptexits 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.