Test: TS-07 returning user with existing display_name skips set-name
Inverse coverage for the auth path: TS-01..TS-06 all wipe the customer row (drop_customer=true) so every OTP path lands on the new-user set-name branch. TS-07 instead seeds an existing identified customer (phone + display_name + is_anonymous=false) and verifies the OTP sign-in returns the existing row unchanged via resolveCustomerForIdentity branch 1, so /auth/set-name is never shown. Adds: * /internal/_test/seed-customer endpoint — upserts a customer with phone + display_name + is_anonymous=false. * client_app/.maestro/scripts/seed_customer.js helper. * client_app/.maestro/flows/ts-07_returning_existing_name_skips_setname.yaml. * TS-07 scenario doc + coverage-map row in requirement/phase4-customer-flow.md. The flow asserts the "halo, <name>" greeting on the returning-user home variant (identified users always land on _SHomeReturningView regardless of chat history) plus an explicit notVisible on "Siapa namamu" as a belt-and-braces check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -306,6 +306,29 @@ export const internalTestRoutes = async (fastify) => {
|
||||
return { ok: true, payment_id: row.id, ...row }
|
||||
})
|
||||
|
||||
// Upsert a customer row with phone + display_name (is_anonymous=false).
|
||||
// Used by Maestro TS-07 to set up the "returning user already has a name"
|
||||
// precondition: a real returning OTP sign-in must skip the set-name screen
|
||||
// because resolveCustomerForIdentity returns the existing row unchanged.
|
||||
//
|
||||
// Body: { phone, display_name }
|
||||
fastify.post('/seed-customer', async (request, reply) => {
|
||||
const phone = request.body?.phone
|
||||
const display_name = request.body?.display_name
|
||||
if (!phone || !display_name) {
|
||||
return reply.code(400).send({ error: 'phone and display_name required in body' })
|
||||
}
|
||||
const [row] = await sql`
|
||||
INSERT INTO customers (phone, display_name, is_anonymous)
|
||||
VALUES (${phone}, ${display_name}, false)
|
||||
ON CONFLICT (phone) DO UPDATE
|
||||
SET display_name = EXCLUDED.display_name,
|
||||
is_anonymous = false
|
||||
RETURNING id, phone, display_name, is_anonymous
|
||||
`
|
||||
return { ok: true, ...row }
|
||||
})
|
||||
|
||||
// Mark EVERY mitra row online. Used by Maestro flows as a setup step to
|
||||
// ensure a clean known-good state regardless of what previous tests did
|
||||
// (e.g. force-mitra-offline leaving the dev DB with no online mitras).
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# TS-07 — Returning user with existing display_name skips set-name screen
|
||||
# (requirement/phase4-customer-flow.md → Test Scenarios → TS-07).
|
||||
#
|
||||
# Inverse of TS-01..TS-06: those flows wipe the customer (drop_customer=true)
|
||||
# so every OTP path hits the new-user set-name branch. TS-07 instead seeds
|
||||
# an EXISTING customer row with phone + display_name, then verifies the
|
||||
# OTP sign-in returns the existing row unchanged (via
|
||||
# resolveCustomerForIdentity branch 1) and the client routes directly to
|
||||
# /home without showing /auth/set-name.
|
||||
#
|
||||
# Pre-reqs:
|
||||
# - Backend reachable, NODE_ENV != 'production'.
|
||||
# - (No mitra requirement — flow stops at /home.)
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/ts-07_returning_existing_name_skips_setname.yaml
|
||||
appId: com.halobestie.client.client_app
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
EXISTING_NAME: "Returning User"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
---
|
||||
# --- Setup: wipe the phone, then re-seed an identified customer with name ---
|
||||
- runScript:
|
||||
file: ../scripts/reset_phone.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- runScript:
|
||||
file: ../scripts/seed_customer.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
DISPLAY_NAME: ${EXISTING_NAME}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- launchApp:
|
||||
clearState: true
|
||||
|
||||
# --- Welcome carousel → home (anon) ---
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "Mulai"
|
||||
timeout: 15000
|
||||
- tapOn:
|
||||
text: "Mulai"
|
||||
retryTapIfNoChange: true
|
||||
|
||||
# Home anon view shows the `masuk →` banner.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*udah pernah pakai HaloBestie.*"
|
||||
timeout: 30000
|
||||
|
||||
# --- Tap masuk → register → phone → OTP ---
|
||||
- tapOn:
|
||||
text: "(?s).*masuk →.*"
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*nomor wa-mu.*"
|
||||
timeout: 10000
|
||||
- tapOn:
|
||||
point: "60%, 47%"
|
||||
- inputText: "81234567890"
|
||||
- hideKeyboard
|
||||
- tapOn:
|
||||
text: "(?s).*kirim kode.*"
|
||||
|
||||
- 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}
|
||||
|
||||
# --- KEY ASSERTIONS ---
|
||||
# 1. OTP entry should disappear (auto-submit on 6th digit).
|
||||
- extendedWaitUntil:
|
||||
notVisible:
|
||||
text: "Masukkan OTP"
|
||||
timeout: 15000
|
||||
|
||||
# 2. Home renders directly. Identified (verified) users land on the
|
||||
# `_SHomeReturningView` regardless of chat history: greeting becomes
|
||||
# "halo, <name>" and CTA flips to "curhat sama bestie baru". The
|
||||
# 1st-time view ("aku mau curhat") is the anon-user variant only.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*halo, ${EXISTING_NAME}.*"
|
||||
timeout: 20000
|
||||
- assertVisible: "(?s).*curhat sama bestie baru.*"
|
||||
|
||||
# 3. The "Siapa namamu?" set-name screen must NOT have been shown —
|
||||
# if it had, the assertion above would have failed at the set-name
|
||||
# intermediate. This belt-and-braces assert catches the case where
|
||||
# the set-name screen briefly flashes then auto-redirects.
|
||||
- assertNotVisible:
|
||||
text: "(?s).*Siapa namamu.*"
|
||||
20
client_app/.maestro/scripts/seed_customer.js
Normal file
20
client_app/.maestro/scripts/seed_customer.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// Upsert a customer row with TEST_PHONE + DISPLAY_NAME via the dev-only
|
||||
// /internal/_test/seed-customer endpoint. Used by TS-07 to set up the
|
||||
// "returning user already has a name" precondition, so the OTP sign-in
|
||||
// path can verify the set-name screen is skipped for existing identified
|
||||
// customers.
|
||||
const phone = TEST_PHONE
|
||||
const displayName = DISPLAY_NAME
|
||||
const url = BACKEND_INTERNAL_URL || 'http://localhost:3001'
|
||||
if (!phone) throw new Error('TEST_PHONE env not set')
|
||||
if (!displayName) throw new Error('DISPLAY_NAME env not set')
|
||||
const resp = http.post(`${url}/internal/_test/seed-customer`, {
|
||||
body: JSON.stringify({ phone, display_name: displayName }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(`seed-customer failed (${resp.status}): ${resp.body}`)
|
||||
}
|
||||
const data = json(resp.body)
|
||||
output.CUSTOMER_ID = data.id
|
||||
output.CUSTOMER_DISPLAY_NAME = data.display_name
|
||||
@@ -608,7 +608,7 @@ Manual reproduction checklists for Phase 4 customer flows. Tick boxes as
|
||||
verified. Cluster tag `[C]` = client_app, `[BE]` = backend setup.
|
||||
|
||||
> **Coverage map** — these scenarios collectively exercise every branching
|
||||
> point in §4 of `flow_customer.mermaid.md`:
|
||||
> point in §4 of `flow_customer.mermaid.md`, plus one §2 (auth) edge:
|
||||
>
|
||||
> | Branching point | Scenario(s) |
|
||||
> |---|---|
|
||||
@@ -618,6 +618,7 @@ verified. Cluster tag `[C]` = client_app, `[BE]` = backend setup.
|
||||
> | PayStat: `paid` vs `timeout 20 min` | TS-01/02/04/06 vs TS-05 |
|
||||
> | PairRoute: `lama (Targeted)` vs `baru / cari lain (BlastFlow)` | TS-01/05/06 vs TS-02/04 |
|
||||
> | TargetedRes: `accept` vs `reject/timeout` | TS-01/05 vs TS-06 |
|
||||
> | §2 post-OTP: new user (set-name) vs existing user with name (skip) | TS-01..06 vs TS-07 |
|
||||
|
||||
## TS-01 — Returning user re-pays an online bestie (lama happy path)
|
||||
|
||||
@@ -878,3 +879,55 @@ escape (same shape as TS-03), but post-payment — the customer has already
|
||||
paid, so this is effectively abandoning a paid session. Worth confirming
|
||||
the UX (probably a confirmation prompt) and whether the payment is
|
||||
refunded / converted to credit.
|
||||
|
||||
---
|
||||
|
||||
## TS-07 — Returning user with existing display_name skips set-name screen
|
||||
|
||||
**Flow:** §2 (verified path) `Choice → "verif WA" → OTP → user lookup → existing account (display_name set, has_transacted=false) → /home`. Verifies the existing-user-with-name branch of `resolveCustomerForIdentity`.
|
||||
|
||||
**Affects:** `client_app`, `backend`.
|
||||
|
||||
**Goal:** Confirm a phone-OTP sign-in for a customer who already has a
|
||||
non-empty `display_name` in `customers` does NOT re-show the
|
||||
"Siapa namamu?" set-name screen. Routes directly from OTP success
|
||||
to /home with the stored display_name. This is the inverse of TS-01..TS-06,
|
||||
all of which use `drop_customer:true` (wiping the row) and therefore always
|
||||
land on the new-user set-name branch.
|
||||
|
||||
**Pre-reqs**
|
||||
- [ ] **[BE]** Backend reachable; NODE_ENV != 'production'.
|
||||
|
||||
**Steps**
|
||||
1. [ ] **[BE]** Wipe phone state via `/internal/_test/reset-phone`
|
||||
`{ phone, drop_customer: true }` — clears any prior customer row.
|
||||
2. [ ] **[BE]** Seed an identified customer via
|
||||
`/internal/_test/seed-customer` `{ phone, display_name }` —
|
||||
inserts a row with `is_anonymous=false` and the chosen display_name.
|
||||
3. [ ] **[C]** Cold-launch `client_app` with clearState → welcome
|
||||
carousel → tap `Mulai` → home (anonymous view, shows `masuk →` banner).
|
||||
4. [ ] **[C]** Tap `masuk →` → `/auth/register` → input phone digits
|
||||
(after the `+62` chip) → tap `kirim kode` → OTP screen.
|
||||
5. [ ] **[C]** Peek OTP from the stub, input it — auto-submits on the
|
||||
6th digit.
|
||||
|
||||
**Expected result**
|
||||
- [ ] **[C]** App routes directly to `/home`, CTA `aku mau curhat`
|
||||
visible (the `_SHome1stView` no-history variant). The customer's
|
||||
stored display_name is loaded into the profile state.
|
||||
- [ ] **[C]** The `Siapa namamu?` set-name screen is **never shown**.
|
||||
An `assertNotVisible` for the set-name title at the home-arrival point
|
||||
acts as a belt-and-braces check against a brief flash-then-redirect.
|
||||
- [ ] **[BE]** No new `customers` row created — the seeded row is the
|
||||
same one returned by `getCustomerByPhone` → `resolveCustomerForIdentity`
|
||||
branch 1 (existing identity, no anon prefix). `customers.id` after the
|
||||
flow equals the seeded `CUSTOMER_ID`.
|
||||
|
||||
**Why this needs its own test:** TS-01..TS-06 all begin with
|
||||
`reset_phone` `drop_customer:true`, which makes every OTP path land in
|
||||
`resolveCustomerForIdentity` branch 4 (no existing + no anon → create
|
||||
new with display_name=null → client routes to set-name). That covers
|
||||
the new-user surface but never exercises the "existing user with name"
|
||||
path. TS-07 is the symmetric coverage for the same auth code, ensuring
|
||||
the set-name screen isn't accidentally re-shown for known users (which
|
||||
would be a real UX regression — name re-entry every login).
|
||||
|
||||
Reference in New Issue
Block a user