# GMD Smoke Test Scenarios (Phase 3)

## Purpose

This document defines the detailed scenarios for the E2E smoke test suite that will run on Gradle Managed Devices (GMD). These tests verify critical happy-path user journeys on a real Android OS, complementing the comprehensive Robolectric-based integration tests that cover feature-level correctness.

Smoke tests are **not** intended to replace integration tests. They exist to catch issues that only surface on a real Android environment: actual Activity lifecycle, real filesystem access via SAF, real Compose rendering pipeline, and cross-screen navigation with real back-stack behavior.

## Scope & Constraints

- **Runner:** `AndroidJUnit4` with `createAndroidComposeRule<MainActivity>()`
- **Device:** Gradle Managed Device — Pixel 2, API 33 (aosp)
- **Execution time budget:** The entire smoke suite should complete in under 2 minutes on a single GMD instance
- **No external dependencies:** Tests must not require network, Google Play Billing, or pre-existing device state
- **Test file:** `app/src/androidTest/java/com/yorvana/SmokeTest.kt` — single canonical entry point (replaces the instrumented `app/src/androidTest/java/com/yorvana/MainActivityTest.kt`)
- **Coverage baseline:** Output is captured as `app/coverage-baselines/gmd_smoke.ec` via the `generateGmdCoverage` Gradle task

## Test Design Principles

1. **Two state strategies** — the core happy path (S01–S06) runs as a single multi-step flow test that accumulates state within one Activity launch; all other scenarios (S07–S17) are independent tests that reset DataStore and vault state in `@Before` and use setup helpers to reach the required precondition. See "Shared State vs. Independent Tests" in Implementation Notes for rationale.
2. **Semantic selectors** — use `testTag`, `contentDescription`, and visible text; avoid index-based selectors
3. **Wait-for-idle** — use `waitUntil` / `waitForIdle` instead of fixed delays
4. **Minimal assertions** — verify the screen rendered and key elements are present; leave exhaustive validation to Robolectric tests

---

## Scenarios

### S01: First Launch — Setup Screen

**Precondition:** Fresh install, no vault configured.

**Flow:**
1. App launches to `SetupScreen`
2. Verify "Welcome" title text is displayed
3. Verify setup description text is displayed
4. Verify "Choose Folder" button is visible and enabled

**Verifies:** App cold-starts successfully, correct entry point for first-time users, `MainActivity` resolves the start destination to `Setup`.

---

### S02: Vault Configuration & Sentry Opt-in

**Precondition:** App is on `SetupScreen`.

**Flow:**
1. Tap "Choose Folder" button — SAF folder picker launches
2. Select or create a folder in the picker
3. App moves to Setup Step 2 (Sentry opt-in)
4. Verify "Help improve the app" title and toggle are visible
5. Toggle the switch and tap "Complete Setup"
6. App navigates to `VehicleListScreen` ("My Garage")
7. Verify the "My Garage" title is displayed

**Verifies:** SAF integration works, 2-step setup flow is functional, vault URI and Sentry preference are persisted, `YorvanaApplication.applyVaultUri()` initializes storage correctly.

**Note:** Interacting with the system SAF picker requires UiAutomator (`UiDevice`). If SAF interaction proves flaky or overly complex, this scenario can be simplified to only verify the picker launches (intent is fired) and the remaining flow can be tested by pre-configuring the vault in a `@Before` method using test fixtures.

---

### S03: Add a Vehicle

**Precondition:** Vault is configured, `VehicleListScreen` is displayed with empty state.

**Flow:**
1. Tap the FAB / "Add Vehicle" button
2. `AddEditVehicleScreen` is displayed
3. Verify the "Add Vehicle" title is shown
4. Enter a nickname (e.g., "Test Car") in the Nickname field
5. Optionally fill Make (e.g., "Toyota") and Year (e.g., "2020")
6. Tap the "Save" button
7. App navigates back to `VehicleListScreen`
8. Verify "Test Car" appears in the vehicle list
9. Verify the empty state is no longer shown

**Verifies:** Navigation to AddEditVehicle, form input works, vehicle is persisted to the vault filesystem, vehicle list updates on return, denormalized stats are initialized.

---

### S04: View Vehicle Detail

**Precondition:** At least one vehicle ("Test Car") exists in the list.

**Flow:**
1. Tap on "Test Car" in the vehicle list
2. `VehicleDetailScreen` is displayed
3. Verify "Test Car" appears as the vehicle name/title
4. Verify the empty records state ("No service records yet…") is shown
5. Verify the "Add Record" FAB / button is visible

**Verifies:** Navigation from VehicleList to VehicleDetail with correct vehicle ID, vehicle data loads from storage, empty records state renders.

---

### S05: Add a Service Record

**Precondition:** On `VehicleDetailScreen` for "Test Car" with no records.

**Flow:**
1. Tap the "Add Record" FAB / button
2. `AddEditRecordScreen` is displayed
3. Verify the "Add Record" title is shown
4. Verify the date field is non-empty and contains a valid, parseable date (do not assert an exact "today" string — it can be flaky around midnight, timezone, or locale changes; instead compare against the same formatter the app uses, or simply assert the field is populated)
5. Enter an odometer reading (e.g., "50000")
6. Select a service category from the dropdown (e.g., "Oil Change")
7. Enter a performer (e.g., "Myself")
8. Optionally enter a cost (e.g., "49.99")
9. Tap the "Save" button
10. App navigates back to `VehicleDetailScreen`
11. Verify the new record appears in the records list (showing "Oil Change" and/or the date)
12. Verify the empty records state is no longer shown

**Verifies:** Navigation to AddEditRecord, form fields render with defaults, category selector works, record is persisted to vault filesystem, vehicle detail updates on return.

---

### S06: View Record Detail

**Precondition:** On `VehicleDetailScreen` with at least one record.

**Flow:**
1. Tap on the service record in the list
2. `RecordDetailScreen` is displayed
3. Verify the record date is shown
4. Verify the odometer reading is displayed — assert on stable pieces (e.g., the raw number via substring/regex and the unit separately) rather than an exact formatted string like "50,000 km", since formatting is locale-dependent (thousands separator, spacing). Set the locale and unit deterministically in test setup if an exact match is needed.
5. Verify the category ("Oil Change") is displayed
6. Verify the performer ("Myself") is displayed
7. Verify the cost is displayed (if entered)

**Verifies:** Navigation from VehicleDetail to RecordDetail, record data loads correctly from storage, all fields render.

---

### S07: Add an Attachment via File Picker

**Precondition:** On `AddEditRecordScreen` (either creating a new record or editing an existing one).

**Flow:**
1. Scroll to the "Attachments" section
2. Verify the "Choose file" button is visible
3. Verify the "Take photo" button is visible
4. Tap the "Choose file" button — system file picker launches
5. Select a file (e.g., a test image or PDF pre-pushed to the device)
6. Verify the attachment thumbnail/entry appears in the Attachments section with the filename
7. Tap "Save"
8. App navigates back to the previous screen

**Verifies:** File picker intent launches correctly, picked file URI is resolved and copied into the vault, attachment metadata is persisted with the record.

**Note:** Selecting a file in the system picker requires UiAutomator (`UiDevice`). A test fixture file should be pre-pushed to the emulator's Downloads folder via an `adb push` step in the test setup or Gradle task. If picker interaction proves flaky, verify the intent fires and test the rest of the attachment flow by injecting a URI directly via the ViewModel event.

---

### S08: View Attachment on Record Detail

**Precondition:** A record with at least one attachment exists (created in S07 or set up via test fixture).

**Flow:**
1. Navigate to `RecordDetailScreen` for the record with attachments
2. Verify the "Attachments" section header is displayed with the correct count (e.g., "Attachments (1)")
3. Verify the attachment card is displayed with the filename
4. For image attachments: verify the thumbnail renders (non-null image content)
5. Tap on an image attachment
6. Verify `ImageViewerScreen` is displayed
7. Press back to return to `RecordDetailScreen`

**Verifies:** Attachments are loaded from vault storage, thumbnails render from content URIs, navigation to ImageViewer works with correct URI parameter, back navigation from ImageViewer returns to RecordDetail.

---

### S09: Remove an Attachment

**Precondition:** On `AddEditRecordScreen` in edit mode for a record that has at least one attachment.

**Flow:**
1. Scroll to the "Attachments" section
2. Verify the existing attachment is displayed
3. Tap the "Remove" button on the attachment
4. Verify a confirmation dialog appears (naming the attachment file)
5. Tap "Delete" / "Confirm" in the dialog
6. Verify the attachment is no longer displayed in the Attachments section
7. Tap "Save"
8. Navigate to `RecordDetailScreen` for the same record
9. Verify the Attachments section is empty or absent

**Verifies:** Attachment removal confirmation guard works, attachment is removed from the UI state, deletion is persisted to vault storage on save, attachment file is cleaned up from the filesystem.

---

### S10: Take a Photo Attachment

**Precondition:** On `AddEditRecordScreen` (creating or editing a record), device has a camera available.

**Flow:**
1. Scroll to the "Attachments" section
2. Tap the "Take photo" button — camera app launches
3. Capture a photo (or simulate capture via test intent)
4. Verify the photo appears as a thumbnail in the Attachments section
5. Tap "Save"

**Verifies:** Camera intent launches correctly with the pre-generated output URI (`FileProviderHelper`), captured photo is resolved and stored in the vault, attachment metadata is persisted.

**Note:** Camera simulation on GMD emulators can be configured via `hw.camera.back = emulated` in the AVD config. If camera interaction is unreliable, verify the intent fires and test attachment persistence via file picker (S07) instead.

---

### S11: Navigate to Settings

**Precondition:** App is on any screen with access to Settings (typically VehicleListScreen via a menu or top-bar icon).

**Flow:**
1. Navigate back to `VehicleListScreen` (if not already there)
2. Tap the Settings icon / menu entry
3. `SettingsScreen` is displayed
4. Verify "Settings" title is shown
5. Verify the odometer unit setting is visible (km/miles)
6. Verify the currency setting is visible
7. Verify the data folder / vault location is displayed

**Verifies:** Navigation to Settings, settings screen renders with persisted preferences, vault URI is displayed correctly.

---

### S12: Navigate to Categories

**Precondition:** App is on `SettingsScreen`.

**Flow:**
1. Tap on the "Categories" menu item / entry
2. `CategoriesScreen` is displayed
3. Verify "Categories" title is shown
4. Verify at least one predefined category is displayed (e.g., "Oil Change")
5. Navigate back to Settings

**Verifies:** Navigation from Settings to Categories, predefined categories are loaded, back navigation works.

---

### S13: Edit a Vehicle

**Precondition:** At least one vehicle exists.

**Flow:**
1. Navigate to `VehicleDetailScreen` for "Test Car"
2. Tap the "Edit" action (toolbar icon or menu)
3. `AddEditVehicleScreen` is displayed in edit mode
4. Verify the Nickname field is pre-filled with "Test Car"
5. Change the nickname to "Updated Car"
6. Tap "Save"
7. App navigates back to `VehicleDetailScreen`
8. Verify the updated name "Updated Car" is displayed

**Verifies:** Edit navigation passes the correct vehicle ID, form pre-populates with existing data, update is persisted.

---

### S14: Edit a Service Record

**Precondition:** At least one record exists for the vehicle.

**Flow:**
1. Navigate to `RecordDetailScreen` for the existing record
2. Tap the "Edit" action
3. `AddEditRecordScreen` is displayed in edit mode
4. Verify the odometer field is pre-filled with "50000"
5. Change the odometer reading to "55000"
6. Tap "Save"
7. App navigates back to `RecordDetailScreen`
8. Verify the updated odometer reading ("55,000") is displayed

**Verifies:** Edit navigation passes correct vehicle and record IDs, form pre-populates, update is persisted.

---

### S15: Delete a Service Record

**Precondition:** On `RecordDetailScreen` for an existing record.

**Flow:**
1. Tap the "Delete" action
2. Verify a confirmation dialog appears
3. Tap "Delete" / "Confirm" in the dialog
4. App navigates back to `VehicleDetailScreen`
5. Verify the deleted record is no longer in the list

**Verifies:** Delete confirmation guard works, record and associated files are removed from vault storage, list updates.

---

### S16: Delete a Vehicle

**Precondition:** At least one vehicle exists, on `VehicleListScreen` or `VehicleDetailScreen`.

**Flow:**
1. Initiate vehicle deletion (long-press, swipe, or menu action depending on current UI)
2. Verify a confirmation dialog appears stating records and attachments will be deleted
3. Tap "Delete" / "Confirm" in the dialog
4. `VehicleListScreen` is displayed
5. Verify the deleted vehicle is no longer in the list
6. Verify the empty state is shown (if no other vehicles exist)

**Verifies:** Destructive action guard, vehicle + all associated records/attachments removed from vault, list updates.

---

### S18: Upgrade Dialog/Sheet on Free Tier (Paywall)

**Precondition:** Vault configured; 1 vehicle present; `setBillingOverride(FORCE_FREE)`.

**Flow:**
1. Launch on `VehicleListScreen`.
2. Tap the "Add Vehicle" FAB.
3. Verify the upgrade info sheet is displayed (assert by `testTag = "paywall-info-sheet"`).
4. Verify "Upgrade to Premium" and "Maybe later" buttons are present.
5. Tap "Maybe later".
6. Verify the sheet dismisses and `VehicleListScreen` is still showing 1 vehicle.

**Verifies:** Free-tier gate triggers at `vehicleCount >= 1 && !isPremium`; `PaywallInfoSheet` renders on the real Compose pipeline; dismiss restores screen state.

---

### S19: Read-Only Status Card and Paywall Enforcement (Paywall)

**Precondition:** Vault configured; 2 vehicles ("Car A", "Car B") pre-populated via test helper; `setBillingOverride(FORCE_FREE)`.

**Flow:**
1. Launch on `VehicleListScreen`.
2. Verify the read-only status card is displayed (`testTag = "paywall-status-card"`); verify the "Learn more" action is present in the card.
3. Attempt swipe-to-dismiss on a vehicle row; verify the vehicle is still present (swipe-delete disabled in read-only).
4. Tap "Car A" → `VehicleDetailScreen`. Verify the status card is present; verify the "Add Record" FAB is visible but disabled (assert `stateDescription`).
5. Tap the Settings icon → `SettingsScreen`. Verify the "Premium" section shows the "Upgrade to Premium" entry.

**Verifies:** `PaywallStatusCard` correctly signals read-only state on `VehicleListScreen`; `AppGate` propagates `isReadOnly` through all ViewModels; write-action UI is suppressed or disabled.

---

### S20: Reactive Lift on Premium Activation (Paywall)

**Precondition:** Vault configured; 2 vehicles present; `setBillingOverride(FORCE_FREE)`.

**Flow:**
1. Launch on `VehicleListScreen`. Verify the read-only status card is displayed.
2. From the test process, call `setBillingOverride(FORCE_PREMIUM)`.
3. `waitUntil` the status card is no longer displayed (timeout 5s — involves DataStore write + Flow combine + recomposition).
4. Tap the "Add Vehicle" FAB and verify it navigates to `AddEditVehicleScreen` (no upgrade sheet).
5. Press back; tap "Car A" → `VehicleDetailScreen`. Verify the status card is absent and the "Add Record" FAB is enabled.

**Verifies:** Reactive lift of read-only mode end-to-end through `BillingManagerImpl` → `AppGate` → ViewModels → UI on the real Compose pipeline. Catches stale-state bugs in the gate's `combine` pipeline that unit tests can miss when scope/dispatcher choices differ from production.

---

### S17: Back Navigation

**Precondition:** App has navigated several screens deep (e.g., VehicleList -> VehicleDetail -> RecordDetail).

**Flow:**
1. Starting from `RecordDetailScreen`
2. Press the system back button or toolbar back arrow
3. Verify `VehicleDetailScreen` is displayed
4. Press back again
5. Verify `VehicleListScreen` is displayed

**Verifies:** Back-stack is maintained correctly through the real Activity lifecycle, navigation transitions render without crashes.

---

### S21: Crash Reporting Opt-in Persists (Sentry)

**Precondition:** Vault configured.

**Flow:**
1. Navigate to `SettingsScreen`.
2. Find the "Help improve the app" toggle and turn it **ON**.
3. Exit the app (or recreate the Activity).
4. Navigate back to `SettingsScreen`.
5. Verify the toggle is still **ON**.
6. Turn the toggle **OFF**.
7. Exit the app (or recreate the Activity).
8. Navigate back to `SettingsScreen`.
9. Verify the toggle is still **OFF**.

**Verifies:** Crash reporting preference persists across sessions via DataStore and correctly reflects in the UI. This protects the plumbing that determines if Sentry initializes on next launch.

---

### S22: Report Bug Screen Rendering

**Precondition:** App is on `SettingsScreen`.

**Flow:**
1. Tap on "Report a bug or suggest a feature".
2. `ReportBugScreen` is displayed.
3. Verify the "Feedback" title is shown.
4. Verify the Summary and Description fields are visible.
5. Tap the "Device information" card and verify it expands to show technical data.
6. Verify the Submit button is present and initially disabled.

**Verifies:** Navigation from Settings to ReportBugScreen, expressive UI components render correctly, and form validation logic is active.


---

## Scenarios Explicitly Out of Scope

The following are **not** covered by smoke tests and are handled by Robolectric integration tests or manual testing:

| Area | Reason |
|---|---|
| **Form validation errors** (empty required fields, VIN duplicates, character limits) | Covered exhaustively by ViewModel unit tests and screen integration tests |
| **Image viewer gestures** (pinch-zoom, pan, double-tap) | Gesture simulation on GMD is unreliable; covered by screenshot tests and Robolectric. Navigation to ImageViewer is covered by S08 |
| **Real Google Play Billing purchase sheet** | Requires the device to be signed into a license-tester account with the app on a Play track. Covered manually per `docs/plan-paywall.md` Phase 8 Track B. |
| **Odometer unit / currency switching** | Settings persistence covered by unit tests; display rendering covered by Robolectric |
| **Data folder relocation** | SAF folder picker interaction; high complexity for smoke tests |
| **Edge cases** (200+ records, 20 vehicles, large attachments) | Performance testing, not smoke testing |
| **Error states** (corrupted JSON, missing files, permission denied) | Defensive behavior covered by unit tests per ADR-006 |

---

## Phased Implementation Plan

### Step 1: Test Infrastructure & Helpers

**Goal:** Set up the `SmokeTest.kt` file with all necessary test infrastructure before writing scenario logic.

1. **Rename entry point:** Rename `MainActivityTest.kt` to `SmokeTest.kt` (or create `SmokeTest.kt` and delete `MainActivityTest.kt`). Update the class name to `SmokeTest`. Keep the `@RunWith(AndroidJUnit4::class)` runner and `createAndroidComposeRule<MainActivity>()` rule.
2. **Add UiAutomator dependency:** Add `androidx.test.uiautomator:uiautomator` to `androidTestImplementation` in `app/build.gradle.kts` for system UI interactions (SAF picker, camera).
3. **Add test ordering:** Annotate the class with `@FixMethodOrder(MethodSorters.NAME_ASCENDING)` so scenarios execute in `test_01_`, `test_02_` order.
4. **Vault setup helper:** Create a helper method `configureVaultForTesting()` that creates a temporary vault directory on the emulator's internal storage, persists its URI through the app's DataStore-backed preferences (for example via `AppPreferencesStore.setVaultUri(...)`, or by editing the DataStore directly), and then calls `YorvanaApplication.applyVaultUri()` to bypass the SAF picker for most tests.
5. **Navigation helpers:** Create small helper methods for common navigation steps — `navigateToVehicleDetail(name)`, `navigateToAddRecord()`, `navigateToSettings()` — using Compose test semantics (`onNodeWithText`, `performClick`). These reduce duplication across scenarios.
6. **Attachment fixtures:** Create `app/src/androidTest/assets/` directory with a small test image (`test_receipt.jpg`, ~10KB) and a test PDF (`test_invoice.pdf`, ~5KB). Add a `@Before` or helper that copies these into a picker-visible location for SAF simulation — on API 33, prefer inserting them into Downloads via `MediaStore` (with unique test-specific names) rather than using an app-private directory such as `context.cacheDir`, which DocumentsUI generally cannot browse.
7. **Cleanup rule:** Add a `@After` (or `TestWatcher` rule) that wipes the temporary vault directory and deletes any test fixture entries created in the picker-visible location (for example, the Downloads `MediaStore` items created for the run) to prevent cross-run pollution.
8. **Billing override helper (added with paywall feature, see `docs/plan-paywall.md` Phase 7.5):** Add `setBillingOverride(mode: DebugBillingOverride.Mode)` that calls `(application as YorvanaApplication).debugBillingOverride.setMode(mode)` from the test process. Add an `@After` rule that resets the override to `NONE` to prevent state leaking across scenarios. Add `addVehicleForTesting(nickname)` helper that persists a vehicle directly via `VehicleRepository`, so paywall scenarios can start from a known multi-vehicle state without depending on S03 having run.
9. **Local verification:** Run `./gradlew pixel2api33DebugAndroidTest` to confirm the empty test class compiles, deploys, and the infrastructure helpers initialize without errors.

**Deliverables:** `SmokeTest.kt` with infrastructure, helpers, fixtures, and cleanup — no scenario logic yet. `MainActivityTest.kt` removed.

---

### Step 2: Core Happy Path (S01–S06)

**Goal:** Implement the primary user journey — from first launch through viewing a record — as a single multi-step flow test.

1. **S01 — First launch:** In `test_01_coreHappyPath_flow()`, start from first launch and assert "Welcome" text and "Choose Folder" button are displayed.
2. **S02 — Vault configuration:** In the same flow test, use the hybrid approach: verify the picker intent fires on button tap, then call `configureVaultForTesting()` programmatically and navigate to `VehicleListScreen`. Assert empty state is shown.
3. **S03 — Add vehicle:** Continue in the same flow test. Navigate to AddEditVehicle, fill the nickname field, save, and assert the vehicle appears in the list.
4. **S04 — View vehicle detail:** Continue in the same flow test. Tap the vehicle card and assert the detail screen renders with the vehicle name and empty records state.
5. **S05 — Add service record:** Continue in the same flow test. Navigate to AddEditRecord, fill odometer/category/performer, save, and assert the record appears in the vehicle detail list.
6. **S06 — View record detail:** Finish the same flow test by tapping the record and asserting all fields render on `RecordDetailScreen`.
7. **Local verification:** Run the full suite on GMD. Ensure the single S01–S06 flow test passes reliably end-to-end. Address any timing issues with `waitUntil` / `waitForIdle`.

**Deliverables:** 1 passing multi-step flow test covering the core CRUD happy path from S01 through S06. This is the minimum viable smoke suite.

---

### Step 3: Attachment Scenarios (S07–S10)

**Goal:** Add attachment lifecycle coverage — the primary reason these tests must run on a real device.

1. **S07 — Add attachment via file picker:** Implement `test_07_addAttachmentViaFilePicker()`. Start by creating or editing a record. Use UiAutomator to interact with the system file picker (or use `ActivityResultLauncher` test doubles via `ActivityScenario` if UiAutomator is too fragile). Select the fixture image from the known device path. Assert the attachment thumbnail appears. Save the record.
2. **S08 — View attachment on record detail:** Implement `test_08_viewAttachmentOnRecordDetail()`. Navigate to RecordDetailScreen for the record with attachments. Assert the "Attachments (1)" header and attachment card are displayed. Tap the image attachment, assert `ImageViewerScreen` is displayed, press back.
3. **S09 — Remove attachment:** Implement `test_09_removeAttachment()`. Edit the record, tap "Remove" on the attachment, confirm the dialog, assert the attachment is gone, save. Navigate to RecordDetail and verify the Attachments section is absent or empty.
4. **S10 — Take photo attachment:** Implement `test_10_takePhotoAttachment()`. On AddEditRecordScreen, tap "Take photo", interact with the emulated camera (or stub the camera result). Assert the photo thumbnail appears. Save.
5. **Flakiness assessment:** Run the attachment tests 5 times locally. If S10 (camera) fails >20% of the time, downgrade it to a "picker launch verification only" test and document the limitation.
6. **Local verification:** Full suite (S01–S10) passes on GMD.

**Deliverables:** 4 attachment tests. Camera test may be marked as best-effort depending on emulator reliability.

---

### Step 4: Navigation & Settings (S11–S12, S17)

**Goal:** Cover settings screens and back-stack navigation.

1. **S11 — Navigate to Settings:** Implement `test_11_navigateToSettings()`. From VehicleListScreen, tap Settings, assert the settings screen renders with odometer unit, currency, and vault location.
2. **S12 — Navigate to Categories:** Implement `test_12_navigateToCategories()`. From SettingsScreen, tap Categories, assert predefined categories are listed, navigate back.
3. **S17 — Back navigation:** Implement `test_17_backNavigation()`. Navigate VehicleList → VehicleDetail → RecordDetail, then press back twice, asserting each intermediate screen.
4. **Local verification:** Full suite passes.

**Deliverables:** 3 tests covering settings/categories screens and back-stack integrity.

---

### Step 5: Edit & Delete Flows (S13–S16)

**Goal:** Cover mutation and deletion paths with confirmation guards.

1. **S13 — Edit vehicle:** Implement `test_13_editVehicle()`. Navigate to VehicleDetail, tap Edit, verify pre-filled nickname, change it, save, assert updated name on VehicleDetail.
2. **S14 — Edit service record:** Implement `test_14_editServiceRecord()`. Navigate to RecordDetail, tap Edit, verify pre-filled odometer, change it, save, assert updated value on RecordDetail.
3. **S15 — Delete service record:** Implement `test_15_deleteServiceRecord()`. On RecordDetail, tap Delete, confirm dialog, assert return to VehicleDetail with record removed.
4. **S16 — Delete vehicle:** Implement `test_16_deleteVehicle()`. Initiate vehicle deletion, confirm dialog, assert return to VehicleList with vehicle removed and empty state shown.
5. **Ordering consideration:** S15 and S16 are destructive. If using shared state (approach 1), these must run last. If the test data from S03/S05 was already consumed by prior tests, create fresh test data in a setup helper before running the delete scenarios.
6. **Local verification:** Full suite (S01–S17) passes on GMD.

**Deliverables:** 4 tests covering edit and delete flows. Complete smoke suite of 17 scenarios.

---

### Step 6: Paywall Scenarios (S18–S20)

**Goal:** Cover paywall gate, read-only banner propagation, and reactive lift on premium activation. Depends on `DebugBillingOverride` shipping with the paywall feature (`docs/plan-paywall.md` Phase 3.3) — sequence this step **after** that work.

1. **S18 — Upgrade dialog on free tier:** Implement `test_18_upgradeDialogOnFreeTier()`. Setup: 1 vehicle + `FORCE_FREE`. Tap FAB; assert dialog renders; dismiss; assert dialog gone.
2. **S19 — Read-only banner across screens:** Implement `test_19_readOnlyBannerAcrossScreens()`. Setup: 2 vehicles + `FORCE_FREE`. Visit VehicleList → VehicleDetail → Settings; assert banner present on each; assert write actions suppressed.
3. **S20 — Read-only lifts on premium activation:** Implement `test_20_readOnlyLiftsOnPremium()`. Setup: 2 vehicles + `FORCE_FREE`. Switch override to `FORCE_PREMIUM` from the test process; `waitUntil` banner disappears (timeout 5s); assert FAB now navigates to AddVehicle.
4. **Override reset:** Confirm `@After` resets override to `NONE` so subsequent tests are unaffected.
5. **Local verification:** Full suite (S01–S20) passes on GMD; verify paywall tests do not contaminate non-paywall scenarios.

**Deliverables:** 3 paywall smoke tests. Suite total: 20 scenarios.

---

### Step 7: Coverage Baseline & Final Verification

**Goal:** Generate the coverage baseline and validate the complete suite.

1. **Full local run:** Run `./gradlew pixel2api33DebugAndroidTest` and confirm all 17 tests pass.
2. **Timing check:** Verify the entire suite completes within the 2-minute budget. If it exceeds the budget, identify the slowest scenarios and consider merging related scenarios (e.g., combine S11+S12 into a single settings navigation test).
3. **Generate coverage:** Run `./gradlew generateGmdCoverage` to produce the updated `gmd_smoke.ec` baseline.
4. **Coverage report:** Run `./gradlew verifyWithCoverage` and review the report. Confirm the smoke tests contribute meaningful coverage to areas not covered by Robolectric (Activity lifecycle, SAF integration, real filesystem paths).
5. **Commit:** Commit `SmokeTest.kt`, attachment fixtures, dependency changes, and the updated `gmd_smoke.ec` to Git LFS.
6. **Update documentation:** Update `TESTING_SETUP.md` to document the smoke test suite, its scenarios, and how to run/maintain it.

**Deliverables:** Final smoke suite committed, coverage baseline refreshed, documentation updated. Phase 3 complete.

---

## Implementation Notes

### Shared State vs. Independent Tests

Two viable strategies:

1. **Single flow test** — implement the core happy-path journey as one `@Test` method with multiple sequential steps. With `createAndroidComposeRule<MainActivity>()`, this is the only approach that truly keeps a single Activity launch for the entire chain. Fastest for the end-to-end path, but failures stop the remainder of that flow.

2. **Independent tests with setup helpers** — each `@Test` launches a fresh `MainActivity` and uses helper methods to reach the required precondition state. If tests are ordered, they may still share persisted state such as vault/files on disk, but they do **not** share one in-memory Activity instance. Slower (multiple launches) but more robust.

**Recommendation:** Use approach 1 for the core happy-path chain (S01-S06) as a single multi-step flow test, and approach 2 for isolated scenarios (S07-S17) that should remain independent of prior in-memory UI state.

### SAF Picker Interaction

The SAF folder picker (S02) is a system UI outside the app's Compose tree. Options:
- **UiAutomator** (`UiDevice.findObject()`) to interact with the system picker — most realistic but fragile across API levels
- **Pre-configure vault** in a `@Before` / test fixture by writing the vault URI via DataStore (for example, `AppPreferencesStore.setVaultUri`, backed by `Context.dataStore`) and then calling `YorvanaApplication.applyVaultUri()` — skips SAF but tests everything after it
- **Hybrid** — one dedicated test for SAF launch verification, remaining tests use the DataStore-preconfigured vault

**Recommendation:** Use the hybrid approach. One test verifies the picker launches; all others pre-configure the vault via DataStore.

### Attachment Test Fixtures

Attachment scenarios (S07-S10) require files to be available on the emulator. Options:
- **Pre-push via Gradle:** Add a task that runs `adb push` to place a small test image (e.g., `test_receipt.jpg`) and a test PDF into the emulator's Downloads folder before tests execute
- **Bundle in `androidTest/assets`:** Place fixture files in `app/src/androidTest/assets/` and copy them to a known device path in `@Before`
- **Programmatic creation:** Generate a minimal bitmap or text file in `@Before` and write it to the device filesystem

**Recommendation:** Bundle fixtures in `androidTest/assets` — this is self-contained, doesn't require `adb`, and works reliably on GMD.

### Test Data Cleanup

Tests that create vehicles/records should clean up after themselves or use a temporary vault directory that is wiped in `@After`. This prevents test pollution across runs and ensures `generateGmdCoverage` produces consistent results.
