Skip to content

Commit e6628e3

Browse files
committed
feat: implement deep linking for atomic mobile testing and add comprehensive documentation for web and mobile test automation.
1 parent 738478e commit e6628e3

File tree

9 files changed

+875
-23
lines changed

9 files changed

+875
-23
lines changed

ATOMIC_MOBILE_TESTING.md

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
# Atomic Mobile Testing — Deep Link Support
2+
3+
Enables the external automation framework (`ahm-poc`) to open the OmniPizza mobile
4+
app directly on any target screen after hydrating state through the API, without
5+
executing a full user journey.
6+
7+
---
8+
9+
## 1. Repository Findings
10+
11+
| Item | Details |
12+
|------|---------|
13+
| Entry point | `frontend-mobile/App.tsx``NavigationContainer` wrapping a single `createNativeStackNavigator` |
14+
| Screens | 6: Login, Catalog, PizzaBuilder, Checkout, OrderSuccess, Profile |
15+
| State | Zustand (`useAppStore`) — ephemeral, no persistence layer |
16+
| URI scheme | `omnipizza://` already registered in `app.json` |
17+
| Deep linking before | Not implemented (scheme registered but no handlers) |
18+
| Cart hydration | Exists in `CheckoutScreen` — skips fetch if local cart is non-empty |
19+
20+
---
21+
22+
## 2. Solution Summary
23+
24+
React Navigation's built-in `linking` prop on `NavigationContainer` maps
25+
`omnipizza://<route>` URLs directly to screens, passing all query params as
26+
screen route params.
27+
28+
A separate `useDeepLinkParams` hook processes the side-effect params that
29+
Navigation cannot handle on its own:
30+
- `market` / `lang` → update Zustand store (country + language)
31+
- `resetSession=true``logout()` + navigate to Login
32+
- `hydrateCart=true``clearCart()` so `CheckoutScreen`'s existing hydration runs
33+
34+
`PizzaBuilderScreen` was extended with a minimal `pizzaId` + `size` fallback so it
35+
can be reached atomically by ID instead of requiring a full `Pizza` object.
36+
37+
---
38+
39+
## 3. Files Modified
40+
41+
| File | Type | Why |
42+
|------|------|-----|
43+
| `frontend-mobile/src/navigation/types.ts` | **Created** | Typed `RootStackParamList` with deep link params for every screen |
44+
| `frontend-mobile/src/navigation/linking.ts` | **Created** | React Navigation `LinkingOptions` mapping `omnipizza://` routes to screen names |
45+
| `frontend-mobile/src/hooks/useDeepLinkParams.ts` | **Created** | Hook that handles state side effects from URL params (market, lang, resetSession, hydrateCart) |
46+
| `frontend-mobile/App.tsx` | **Modified** | Added `linking` prop to `NavigationContainer`, wired `useDeepLinkParams`, exported `navigationRef` |
47+
| `frontend-mobile/src/screens/PizzaBuilderScreen.tsx` | **Modified** | Handle `pizzaId` and `size` deep link params as fallback when no full `pizza` object is passed |
48+
49+
---
50+
51+
## 4. Deep Link Reference
52+
53+
### Supported routes
54+
55+
| Deep Link | Opens Screen | Notes |
56+
|-----------|-------------|-------|
57+
| `omnipizza://login` | LoginScreen | Always accessible |
58+
| `omnipizza://catalog` | CatalogScreen | Requires valid token in store |
59+
| `omnipizza://pizza-builder` | PizzaBuilderScreen | Requires `pizzaId` param |
60+
| `omnipizza://checkout` | CheckoutScreen | Use `hydrateCart=true` for API-injected carts |
61+
| `omnipizza://order-success` | OrderSuccessScreen | Pass `orderId` for test assertions |
62+
| `omnipizza://profile` | ProfileScreen | Requires valid token in store |
63+
64+
### Universal query params (any route)
65+
66+
| Param | Values | Effect |
67+
|-------|--------|--------|
68+
| `market` | `US` `MX` `CH` `JP` | Sets country in store; auto-updates language |
69+
| `lang` | `de` `fr` | Sets CH language preference (ignored for other markets) |
70+
| `resetSession` | `true` | Logs out, clears cart, navigates to Login |
71+
72+
### Route-specific params
73+
74+
| Route | Param | Effect |
75+
|-------|-------|--------|
76+
| `pizza-builder` | `pizzaId=<id>` | Fetches pizza from catalog by ID |
77+
| `pizza-builder` | `size=small\|medium\|large\|family` | Pre-selects size |
78+
| `checkout` | `hydrateCart=true` | Clears local cart so screen fetches from backend |
79+
| `order-success` | `orderId=<id>` | Available in route params for test assertions |
80+
81+
---
82+
83+
## 5. Deep Link Examples
84+
85+
```
86+
# Open Login (cold reset before test suite)
87+
omnipizza://login?resetSession=true
88+
89+
# Open Catalog in JP market
90+
omnipizza://catalog?market=JP&lang=ja
91+
92+
# Open PizzaBuilder with a specific pizza pre-loaded, family size, MX market
93+
omnipizza://pizza-builder?pizzaId=pepperoni&size=family&market=MX&lang=es
94+
95+
# Open Checkout with API-injected cart (requires POST /api/cart first)
96+
omnipizza://checkout?market=US&lang=en&hydrateCart=true
97+
98+
# Open Checkout in Switzerland, French language
99+
omnipizza://checkout?market=CH&lang=fr&hydrateCart=true
100+
101+
# Open OrderSuccess with order ID for assertion
102+
omnipizza://order-success?orderId=12345&market=JP&lang=ja
103+
104+
# Open Profile in US market
105+
omnipizza://profile?market=US&lang=en
106+
```
107+
108+
---
109+
110+
## 6. External Automation Usage (`ahm-poc`)
111+
112+
The pattern for each atomic test:
113+
114+
```
115+
1. API setup
116+
POST /api/auth/login → get token
117+
POST /api/market (header) → set X-Country-Code
118+
POST /api/cart { items: [...] } → seed cart
119+
120+
2. Deep link
121+
driver.execute("mobile: deepLink", {
122+
url: "omnipizza://checkout?market=MX&lang=es&hydrateCart=true",
123+
package: "com.omnipizza.app" // Android
124+
})
125+
126+
// iOS (XCUITest / Appium)
127+
driver.execute("mobile: deepLink", {
128+
url: "omnipizza://checkout?market=MX&lang=es&hydrateCart=true"
129+
})
130+
131+
// Or via adb (Android direct)
132+
adb shell am start \
133+
-W -a android.intent.action.VIEW \
134+
-d "omnipizza://checkout?market=MX&lang=es&hydrateCart=true" \
135+
com.omnipizza.app
136+
137+
// Or via xcrun simctl (iOS simulator)
138+
xcrun simctl openurl booted \
139+
"omnipizza://checkout?market=MX&lang=es&hydrateCart=true"
140+
141+
3. Assert
142+
await expect(element(by.id("screen-checkout"))).toBeVisible();
143+
await expect(element(by.id("view-order-summary"))).toBeVisible();
144+
```
145+
146+
### Flow example — Checkout atomic test (MX market)
147+
148+
```bash
149+
# 1. Login and seed state
150+
TOKEN=$(curl -s -X POST https://omnipizza-backend.onrender.com/api/auth/login \
151+
-H "Content-Type: application/json" \
152+
-d '{"username":"demo_mx","password":"demo123"}' | jq -r .access_token)
153+
154+
curl -s -X POST https://omnipizza-backend.onrender.com/api/cart \
155+
-H "Authorization: Bearer $TOKEN" \
156+
-H "X-Country-Code: MX" \
157+
-H "Content-Type: application/json" \
158+
-d '{"items":[{"pizza_id":"pepperoni","size":"large","quantity":2}]}'
159+
160+
# 2. Set token in app (via Detox launchArgs or Appium capability)
161+
# 3. Open checkout directly
162+
adb shell am start -W -a android.intent.action.VIEW \
163+
-d "omnipizza://checkout?market=MX&lang=es&hydrateCart=true" \
164+
com.omnipizza.app
165+
```
166+
167+
---
168+
169+
## 7. Manual Validation Steps
170+
171+
### iOS Simulator
172+
```bash
173+
xcrun simctl openurl booted omnipizza://login
174+
xcrun simctl openurl booted omnipizza://catalog?market=JP
175+
xcrun simctl openurl booted "omnipizza://pizza-builder?pizzaId=pepperoni&size=large&market=US"
176+
xcrun simctl openurl booted "omnipizza://checkout?market=MX&lang=es&hydrateCart=true"
177+
xcrun simctl openurl booted "omnipizza://order-success?orderId=abc123&market=US"
178+
xcrun simctl openurl booted omnipizza://profile?market=CH&lang=fr
179+
```
180+
181+
### Android Emulator
182+
```bash
183+
adb shell am start -W -a android.intent.action.VIEW -d "omnipizza://login" com.omnipizza.app
184+
adb shell am start -W -a android.intent.action.VIEW -d "omnipizza://catalog?market=JP" com.omnipizza.app
185+
adb shell am start -W -a android.intent.action.VIEW -d "omnipizza://pizza-builder?pizzaId=pepperoni&size=large&market=US" com.omnipizza.app
186+
adb shell am start -W -a android.intent.action.VIEW -d "omnipizza://checkout?market=MX&lang=es&hydrateCart=true" com.omnipizza.app
187+
adb shell am start -W -a android.intent.action.VIEW -d "omnipizza://order-success?orderId=abc123&market=US" com.omnipizza.app
188+
adb shell am start -W -a android.intent.action.VIEW -d "omnipizza://profile?market=CH&lang=fr" com.omnipizza.app
189+
```
190+
191+
### Validation checklist
192+
193+
- [ ] `omnipizza://login` opens LoginScreen directly
194+
- [ ] `omnipizza://catalog?market=JP` opens CatalogScreen and displays JPY prices
195+
- [ ] `omnipizza://pizza-builder?pizzaId=pepperoni&size=large` loads pepperoni pizza with Large pre-selected
196+
- [ ] `omnipizza://pizza-builder?pizzaId=unknown` falls back (goBack → Catalog)
197+
- [ ] `omnipizza://checkout?hydrateCart=true` with seeded backend cart shows correct items
198+
- [ ] `omnipizza://checkout?market=MX` shows MX-specific fields (colonia, zip)
199+
- [ ] `omnipizza://order-success?orderId=12345` opens OrderSuccess screen
200+
- [ ] `omnipizza://profile?market=CH&lang=fr` opens Profile with French locale
201+
- [ ] `omnipizza://login?resetSession=true` clears session and lands on Login
202+
- [ ] Warm start (app already open): deep link switches screen correctly
203+
204+
---
205+
206+
## 8. Technical Notes
207+
208+
### Architecture decisions
209+
210+
**Why React Navigation `linking` + a separate hook?**
211+
React Navigation's `linking` config handles URL-to-screen routing automatically
212+
(both cold start and warm start). The hook handles state mutations that don't map
213+
to screen params — you cannot tell Navigation to "also call `setCountry()`" via URL
214+
mapping alone.
215+
216+
**Why `hydrateCart` clears local cart instead of fetching itself?**
217+
`CheckoutScreen` already has robust cart hydration logic (with error fallback,
218+
cancellation, cart item normalization). Duplicating that in the hook would violate
219+
DRY. The hook just signals intent by clearing the local cart; the screen does the rest.
220+
221+
**Why `resetSession` navigates to Login via `navRef.reset()`?**
222+
`navigation.navigate("Login")` would push Login onto the stack, preserving the
223+
current screen in history. `reset()` replaces the entire stack, which matches the
224+
expected behavior for a full session reset.
225+
226+
### Edge cases
227+
228+
| Scenario | Behavior |
229+
|----------|---------|
230+
| `omnipizza://pizza-builder` with no `pizzaId` | Screen renders `null` (existing behavior — `if (!pizza) return null`) |
231+
| `omnipizza://pizza-builder?pizzaId=<nonexistent>` | Screen calls `navigation.goBack()` |
232+
| `omnipizza://checkout` with empty local cart but no backend cart | Hydration runs, finds nothing, screen renders with empty cart |
233+
| `market=CH&lang=es` | `setCountry("CH")` sets German, then `setLanguage("es")` is ignored (not de/fr) |
234+
| Deep link while unauthenticated | Screen renders normally; screens that require a token will hit API errors and their own fallback UIs |
235+
| Cold start timing | `getInitialURL()` is awaited asynchronously; `navigationRef.isReady()` guard prevents navigation before container mounts |
236+
237+
### No auth guard in deep link handler
238+
239+
Screens requiring authentication (Catalog, Checkout, Profile, etc.) are not
240+
individually guarded by the deep link handler. The screens themselves handle
241+
auth errors through their existing API calls. Adding a centralized auth guard
242+
is possible but would require a meaningful architecture change; for a test/demo
243+
app the current behavior is acceptable.

0 commit comments

Comments
 (0)