Refresh the Flutter snapshot: LAUNCH.md and the iPhone verification pass
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2
RIPPR.md
2
RIPPR.md
@@ -9,7 +9,7 @@ only off-machine copies of their history.
|
|||||||
| `~/dojo/rippr` | The original **native Android** app (Kotlin, v1 → v2.0.1). **Superseded**, but still the only version that has recorded real rides. |
|
| `~/dojo/rippr` | The original **native Android** app (Kotlin, v1 → v2.0.1). **Superseded**, but still the only version that has recorded real rides. |
|
||||||
| `~/dojo/rippr-flutter` | The **Flutter port** — Android *and* iOS. Feature-complete, better tested, never yet ridden. |
|
| `~/dojo/rippr-flutter` | The **Flutter port** — Android *and* iOS. Feature-complete, better tested, never yet ridden. |
|
||||||
|
|
||||||
**Snapshots taken at:** native `ba57a92`, Flutter `82f07a2`.
|
**Snapshots taken at:** native `ba57a92`, Flutter `a0f678a`.
|
||||||
|
|
||||||
## What is here
|
## What is here
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -72,6 +72,8 @@ speed-derived values, where Kotlin's 32-bit `Float` widens with artefacts Dart's
|
|||||||
| Document | Contents |
|
| Document | Contents |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Why it is built this way, and what changed from the native app |
|
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Why it is built this way, and what changed from the native app |
|
||||||
|
| [docs/LAUNCH.md](docs/LAUNCH.md) | Getting from "on my phone" to the app stores, in stages, with costs |
|
||||||
|
| [docs/port/IOS-VERIFICATION.md](docs/port/IOS-VERIFICATION.md) | What a Mac can prove about the iPhone build, and what cannot |
|
||||||
| [docs/port/PLAN.md](docs/port/PLAN.md) | The 28-task migration plan |
|
| [docs/port/PLAN.md](docs/port/PLAN.md) | The 28-task migration plan |
|
||||||
| [docs/port/PROGRESS.md](docs/port/PROGRESS.md) | What actually happened, including every bug found |
|
| [docs/port/PROGRESS.md](docs/port/PROGRESS.md) | What actually happened, including every bug found |
|
||||||
| [docs/port/PARITY-AUDIT.md](docs/port/PARITY-AUDIT.md) | Feature-by-feature, with the evidence behind each claim |
|
| [docs/port/PARITY-AUDIT.md](docs/port/PARITY-AUDIT.md) | Feature-by-feature, with the evidence behind each claim |
|
||||||
|
|||||||
195
rippr-flutter-src/docs/LAUNCH.md
Normal file
195
rippr-flutter-src/docs/LAUNCH.md
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
# Launch
|
||||||
|
|
||||||
|
How Rippr gets from "on my phone" to "on the App Store with money coming in" — in stages,
|
||||||
|
because the jump is much bigger than it looks and most of it is not code.
|
||||||
|
|
||||||
|
**Where things stand:** the app is feature-complete on both platforms and has never
|
||||||
|
recorded a real ride. Nothing below should start before
|
||||||
|
[port/REAL-RIDE-CHECKLIST.md](port/REAL-RIDE-CHECKLIST.md) is done.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 0 — you and your friends *(today, free)*
|
||||||
|
|
||||||
|
What works right now, with no accounts and no stores.
|
||||||
|
|
||||||
|
### Android — genuinely easy
|
||||||
|
|
||||||
|
Send them the APK. That's it.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb install -r rippr-flutter-1.0-debug.apk # or just message them the file
|
||||||
|
```
|
||||||
|
|
||||||
|
They tap it in Files and allow "install unknown apps". It works, it keeps working, and it
|
||||||
|
costs nothing. **For riding buddies on Android, this is a complete answer** — you may
|
||||||
|
never need the Play Store.
|
||||||
|
|
||||||
|
Worth switching to a **release build** before handing it round: the debug APK is ~172 MB
|
||||||
|
and slower. That needs a keystore, kept somewhere you will not lose it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
keytool -genkey -v -keystore ~/rippr-release.jks -keyalg RSA -keysize 2048 \
|
||||||
|
-validity 10000 -alias rippr
|
||||||
|
flutter build apk --release --split-per-abi # ~3 files, ~20 MB each
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Back that keystore up.** Lose it and you can never update an app that shipped with it.
|
||||||
|
|
||||||
|
### iOS — awkward, and the awkwardness is the point
|
||||||
|
|
||||||
|
Free signing (see `INSTALL.md` in the samplez repo, alongside the APK) gives **7-day
|
||||||
|
builds**, three apps at a time, and needs your Mac every time. Fine for you. Miserable for
|
||||||
|
a friend who just wants to ride.
|
||||||
|
|
||||||
|
There is no free way around it. Apple deliberately does not allow casual iPhone
|
||||||
|
sideloading.
|
||||||
|
|
||||||
|
**If any of your friends ride with an iPhone, Stage 1 is not optional.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 1 — a real group of testers *(~$99/yr + $25 once)*
|
||||||
|
|
||||||
|
This is the stage that actually fits "me and my friends", and it is a big quality-of-life
|
||||||
|
jump over Stage 0.
|
||||||
|
|
||||||
|
### TestFlight — the single best upgrade
|
||||||
|
|
||||||
|
Requires the **Apple Developer Program, $99/yr**.
|
||||||
|
|
||||||
|
- Builds last **90 days**, not 7
|
||||||
|
- **100 internal testers** and up to **10,000 external** via a public link
|
||||||
|
- Friends install the TestFlight app and tap once — no Mac, no cables, no trust dialogs
|
||||||
|
- Updates push automatically
|
||||||
|
- External testers need a light App Review pass (usually a day); internal testers do not
|
||||||
|
|
||||||
|
For a riding group this is effectively "the app store, for people I choose". Many projects
|
||||||
|
happily live here forever and never publish publicly.
|
||||||
|
|
||||||
|
### Google Play internal testing — optional
|
||||||
|
|
||||||
|
**$25, one time, forever.** Up to 100 testers, no review for the internal track. Worth it
|
||||||
|
mainly if handing out APKs starts to annoy you, or you want automatic updates on Android
|
||||||
|
too.
|
||||||
|
|
||||||
|
### What you must add before either
|
||||||
|
|
||||||
|
- **A privacy policy URL.** Both stores require one for anything touching location, even
|
||||||
|
in testing. It can be a single static page: what is collected (GPS while recording),
|
||||||
|
where it goes (nowhere — stays on the device), and how to delete it (delete the ride, or
|
||||||
|
the app).
|
||||||
|
- Switch the bundle id from `com.rippr.port` to something you own, e.g. `com.dylan.rippr`.
|
||||||
|
- A release build, on both platforms.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 2 — public launch
|
||||||
|
|
||||||
|
Everything in Stage 1, plus the parts that are genuinely work.
|
||||||
|
|
||||||
|
### Apple
|
||||||
|
|
||||||
|
- **Privacy nutrition labels** — Location → Precise, *not* linked to identity, App
|
||||||
|
Functionality only. Details in [port/RELEASE-IOS.md](port/RELEASE-IOS.md).
|
||||||
|
- **Expect to justify background location.** It is the most common rejection for apps like
|
||||||
|
this. Head it off with a **demo video of a real ride** in the review notes, showing the
|
||||||
|
user pressing Start.
|
||||||
|
- App name, subtitle, keywords, screenshots at several device sizes, a support URL.
|
||||||
|
- Review is typically 24–48 hours, and a rejection round is normal. Budget two weeks.
|
||||||
|
|
||||||
|
### Google
|
||||||
|
|
||||||
|
- **Data Safety form** — the Play equivalent of nutrition labels, and it must match
|
||||||
|
reality.
|
||||||
|
- Background location gets **extra scrutiny on Play**: a written justification and often a
|
||||||
|
video. Rippr's case is strong (a ride recorder is the textbook example), but it is a real
|
||||||
|
review step.
|
||||||
|
- Store listing, feature graphic, screenshots.
|
||||||
|
|
||||||
|
### Things that only matter once strangers use it
|
||||||
|
|
||||||
|
- **Crash reporting.** Currently none — the v3 backlog notes a recorder that dies mid-ride
|
||||||
|
leaves no trace beyond logcat. Sentry or Crashlytics before public launch.
|
||||||
|
- **A support address** people can actually reach.
|
||||||
|
- Whatever the real ride teaches you about battery. Strangers will not forgive what a
|
||||||
|
friend would.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A positioning note, since you ride bikes *and* motorcycles
|
||||||
|
|
||||||
|
Everything user-facing currently says motorcycle, but the recording pipeline is entirely
|
||||||
|
activity-agnostic — it records positions and speeds, and nothing in it cares what you are
|
||||||
|
sitting on. `V3-BACKLOG.md` already carries **activity type per ride** as an idea.
|
||||||
|
|
||||||
|
That matters here rather than only in the backlog, because it decides the store category,
|
||||||
|
the screenshots and who finds the app. Cyclists are a far larger audience than
|
||||||
|
motorcyclists and are already used to paying for ride apps. Deciding whether Rippr is a
|
||||||
|
motorcycle app or a ride recorder is a positioning question worth settling **before**
|
||||||
|
writing a store listing, not after.
|
||||||
|
|
||||||
|
It is also cheap: an activity column on `Trip` and a picker. It needs a real `Migration`,
|
||||||
|
since the destructive fallback is gone.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 3 — monetization
|
||||||
|
|
||||||
|
Only worth designing once people are actually riding with it. But the shape matters
|
||||||
|
because it changes the architecture, and it is worth knowing which door you are opening.
|
||||||
|
|
||||||
|
### The options, from least to most commitment
|
||||||
|
|
||||||
|
**Paid up front — a few dollars, once.**
|
||||||
|
Simplest possible. No servers, no accounts, no recurring obligation. Sells poorly in a
|
||||||
|
category where Strava is free, but the cost to you is nearly zero and it fits an app that
|
||||||
|
works entirely offline.
|
||||||
|
|
||||||
|
**Free, with a one-time unlock.**
|
||||||
|
Recording free forever; pay once for extras — export formats, unlimited history, themes.
|
||||||
|
No servers needed if the extras are local. Good match for Rippr as it exists today.
|
||||||
|
|
||||||
|
**Subscription for cloud features.**
|
||||||
|
The v3 backlog already points here: accounts, cloud backup, group rides. This is the only
|
||||||
|
model that justifies recurring money, because **you have recurring costs**. It is also the
|
||||||
|
biggest jump: a server, authentication, a privacy stance, and an obligation to keep
|
||||||
|
someone's ride history alive.
|
||||||
|
|
||||||
|
> Note the coupling. Cloud backup needs accounts; accounts need a privacy policy with
|
||||||
|
> teeth, GDPR-style deletion, and — on iOS — **in-app account deletion**, which Apple
|
||||||
|
> requires of any app that lets you create an account. Group rides need all of that plus
|
||||||
|
> live location sharing between people, which is a consent question, not a technical one.
|
||||||
|
|
||||||
|
### The numbers
|
||||||
|
|
||||||
|
- **Apple and Google both take 30%**, dropping to **15%** under their small-business
|
||||||
|
programmes (under $1M/yr). Enrol; it is not automatic on Apple's side.
|
||||||
|
- Apple: $99/yr. Google: $25 once.
|
||||||
|
- Server costs are yours. A modest VPS runs a group-ride backend for single-digit dollars
|
||||||
|
a month until it doesn't.
|
||||||
|
- Payments are handled by the stores for digital goods — you may not route around them for
|
||||||
|
in-app features.
|
||||||
|
|
||||||
|
### What I would actually do
|
||||||
|
|
||||||
|
Keep it free while it is you and your friends. If it grows, the honest first step is a
|
||||||
|
**one-time unlock** for local extras — no servers, no obligations, no privacy surface.
|
||||||
|
|
||||||
|
Only build the subscription once someone who is not your friend asks for cloud backup.
|
||||||
|
Paid storage of other people's location history is a commitment that outlives your
|
||||||
|
interest in maintaining it, and that is worth entering deliberately.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The order of operations
|
||||||
|
|
||||||
|
1. **Ride with it.** [port/REAL-RIDE-CHECKLIST.md](port/REAL-RIDE-CHECKLIST.md). Nothing
|
||||||
|
else matters until the numbers are right — and iOS background suspension (item I3) may
|
||||||
|
still force a change of GPS engine.
|
||||||
|
2. Release builds, real bundle id, a keystore you have backed up.
|
||||||
|
3. A one-page privacy policy.
|
||||||
|
4. $99 Apple → TestFlight. Your friends stop needing your Mac.
|
||||||
|
5. Live with it for a season. Let real use decide what is missing.
|
||||||
|
6. Only then: public listings, crash reporting, and a monetization model chosen from
|
||||||
|
evidence rather than from a table like this one.
|
||||||
89
rippr-flutter-src/docs/port/IOS-VERIFICATION.md
Normal file
89
rippr-flutter-src/docs/port/IOS-VERIFICATION.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# iPhone verification — what a Mac can prove, and what needs a phone
|
||||||
|
|
||||||
|
Scoped and then executed. Everything in the first table has been **run**; everything in
|
||||||
|
the second cannot be done without hardware and a rider.
|
||||||
|
|
||||||
|
Reproduce with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcrun simctl location booted start --speed=15 --interval=1 \
|
||||||
|
51.0447,-114.0719 51.0530,-114.0719 51.0610,-114.0800 51.0700,-114.0900 &
|
||||||
|
( for i in $(seq 1 90); do xcrun simctl privacy booted grant location-always com.rippr.port; sleep 1; done ) &
|
||||||
|
flutter test integration_test/ride_simulation_test.dart -d <sim-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The finding that matters most
|
||||||
|
|
||||||
|
**The iOS simulator moves, but still reports zero speed.**
|
||||||
|
|
||||||
|
`simctl location start --speed=15` genuinely interpolates between waypoints over time —
|
||||||
|
unlike Android's `adb emu geo fix`, which teleports. That looked like it might finally
|
||||||
|
make speed testable locally. It does not:
|
||||||
|
|
||||||
|
```
|
||||||
|
RIPPR-PROBE fixes=8 speeds=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||||||
|
```
|
||||||
|
|
||||||
|
Eight fixes, every one zero. Measured, not assumed — the assumption was worth attacking
|
||||||
|
and it survived.
|
||||||
|
|
||||||
|
The consequence is visible in a real recorded ride:
|
||||||
|
|
||||||
|
```
|
||||||
|
trip=1 points=18 distanceM=245.5 maxSpeedKmh=0.0 movingMs=0 segments=1
|
||||||
|
```
|
||||||
|
|
||||||
|
**245 metres travelled, zero moving time.** Distance comes from consecutive positions, so
|
||||||
|
it works. Moving time comes from `speedKmh >= 1.5`, so with speed pinned at zero every
|
||||||
|
sample falls under the noise floor. That is correct behaviour on bad input, and it is
|
||||||
|
exactly why `REAL-RIDE-CHECKLIST.md` cannot be skipped.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Verified locally, on a Mac
|
||||||
|
|
||||||
|
| What | How | Evidence |
|
||||||
|
|---|---|---|
|
||||||
|
| App builds and launches on iOS | `flutter build ios --simulator`, `simctl launch` | Runs |
|
||||||
|
| The real UI renders | Screenshot of the record screen | Wordmark, SPEED headline, Ready state, 72dp button all correct |
|
||||||
|
| **The permission dialog and its usage string** | Screenshot | *"Rippr records the GPS track of your ride… Nothing is recorded until you press Start."* — the Guideline 5.1.1 item, confirmed rendering |
|
||||||
|
| Drift opens against real iOS storage | Integration test | `rippr_db.sqlite` + WAL created in the app container |
|
||||||
|
| Plugin registration resolves | Integration test | geolocator, path_provider, shared_preferences all load |
|
||||||
|
| CoreLocation delivers fixes | Integration test | 18 fixes in ~15 s |
|
||||||
|
| **Distance accumulates from real movement** | Integration test | 245.5 m, latSpan 0.00197° — consistent |
|
||||||
|
| A trip persists and is listed | Integration test | `completedTrips=1`, Card present |
|
||||||
|
| Segments are created | Integration test | `segments=1` |
|
||||||
|
| Trip detail renders, including the map | Integration test | `DISTANCE` found on screen |
|
||||||
|
| Navigation record → rides → detail | Integration test | All transitions land |
|
||||||
|
| Cold start with no ride | Integration test | Lands idle, no crash |
|
||||||
|
| App icon under the iOS mask | Rendered preview | Ring and orange accent survive the superellipse |
|
||||||
|
|
||||||
|
## ⛔ Needs a real iPhone
|
||||||
|
|
||||||
|
| What | Why a simulator cannot |
|
||||||
|
|---|---|
|
||||||
|
| **Max speed, average moving speed** | CoreLocation reports 0.0 — measured above |
|
||||||
|
| **Moving time** | Derived from speed; reads 00:00:00 locally even after 245 m |
|
||||||
|
| **Speed colouring on the map** | Every path renders in one colour |
|
||||||
|
| **Elevation gain** | Simulated altitude has no correlated GPS error; the ~30 m drift question is unanswerable here |
|
||||||
|
| **Background suspension when stationary (I3)** | The simulator does not model iOS's power-management suspension. **This decides whether `geolocator` is sufficient or the paid engine is needed.** |
|
||||||
|
| **Screen-lock continuation** | Real background lifecycle only |
|
||||||
|
| **Phone call / interruption handling** | No telephony |
|
||||||
|
| **Battery drain** | No meaningful power model |
|
||||||
|
| **The blue background-location indicator** | Real background mode only |
|
||||||
|
| **Real GPS accuracy and dropouts** | Simulated fixes are perfect; the accuracy gate never rejects anything |
|
||||||
|
| **App Store review** | Requires submission |
|
||||||
|
|
||||||
|
## 🟡 Gap in local coverage, worth naming
|
||||||
|
|
||||||
|
**No screenshot of the iOS trips list or detail screen.** They are verified
|
||||||
|
*functionally* — the integration test asserts the Card and the `DISTANCE` readout are
|
||||||
|
present — but not *visually*, because tapping the iOS simulator is not scriptable the way
|
||||||
|
`adb shell input tap` is, and every fresh install re-prompts for location, which sits over
|
||||||
|
the UI during the capture window.
|
||||||
|
|
||||||
|
Android's equivalents were checked visually. On iOS this is worth a manual look next time
|
||||||
|
the app is built for a device: launch it, record a short ride by hand, and eyeball the
|
||||||
|
list, the detail stats and the map.
|
||||||
@@ -797,3 +797,36 @@ tests the project has ever had.
|
|||||||
|
|
||||||
**What remains is not code.** It is a rider, two phones, and
|
**What remains is not code.** It is a rider, two phones, and
|
||||||
[REAL-RIDE-CHECKLIST.md](REAL-RIDE-CHECKLIST.md).
|
[REAL-RIDE-CHECKLIST.md](REAL-RIDE-CHECKLIST.md).
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## iPhone verification pass
|
||||||
|
|
||||||
|
Scoped and executed: [IOS-VERIFICATION.md](IOS-VERIFICATION.md).
|
||||||
|
|
||||||
|
**An assumption was attacked and survived.** `simctl location start --speed=15`
|
||||||
|
interpolates between waypoints over time — unlike Android's teleporting `geo fix` — which
|
||||||
|
looked like it might make speed testable locally at last. A probe captured 8 fixes, every
|
||||||
|
one reporting **0.0 m/s**. So "no simulator produces velocity" holds on iOS too, and the
|
||||||
|
real-ride checklist remains mandatory.
|
||||||
|
|
||||||
|
A full ride was recorded on the simulator through the real UI:
|
||||||
|
|
||||||
|
```
|
||||||
|
trip=1 points=18 distanceM=245.5 maxSpeedKmh=0.0 movingMs=0 segments=1
|
||||||
|
```
|
||||||
|
|
||||||
|
245 metres of travel with **zero moving time** — correct behaviour given speed is pinned
|
||||||
|
at zero, and a compact illustration of exactly what local testing cannot reach.
|
||||||
|
|
||||||
|
Also confirmed visually on iOS: the record screen renders correctly, and the location
|
||||||
|
permission dialog displays the specific usage string written for Guideline 5.1.1.
|
||||||
|
|
||||||
|
`integration_test/ride_simulation_test.dart` is kept — it is a reusable local
|
||||||
|
verification, not a throwaway.
|
||||||
|
|
||||||
|
**Named gap:** no screenshot of the iOS trips list or detail screen. Both are verified
|
||||||
|
functionally, but tapping the iOS simulator is not scriptable the way `adb shell input
|
||||||
|
tap` is, and a fresh install re-prompts for location over the UI. Android's equivalents
|
||||||
|
were checked visually.
|
||||||
|
|||||||
89
rippr-flutter-src/integration_test/ride_simulation_test.dart
Normal file
89
rippr-flutter-src/integration_test/ride_simulation_test.dart
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:rippr/main.dart' as app;
|
||||||
|
import 'package:rippr/src/app/providers.dart';
|
||||||
|
|
||||||
|
/// Drives a whole recording on a simulator that is genuinely *moving*.
|
||||||
|
///
|
||||||
|
/// Run `xcrun simctl location booted start --speed=15 <waypoints>` first. The iOS
|
||||||
|
/// simulator interpolates between waypoints over time, so the device genuinely moves —
|
||||||
|
/// but **CoreLocation still reports speed 0.0** throughout. Measured, not assumed: a
|
||||||
|
/// probe run captured 8 fixes, every one of them zero.
|
||||||
|
///
|
||||||
|
/// So position, distance, path shape and segment structure are all verifiable here.
|
||||||
|
/// Speed, moving time and speed colouring are not, on either platform.
|
||||||
|
///
|
||||||
|
/// The app is uninstalled when the run ends, taking its database with it, so anything
|
||||||
|
/// worth knowing is read back through the provider graph and printed from inside.
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
testWidgets('record a ride end to end through the UI', (tester) async {
|
||||||
|
app.main();
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 5));
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('start')));
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 2));
|
||||||
|
|
||||||
|
// Ride for a while, letting the flush timer land batches.
|
||||||
|
for (var i = 0; i < 15; i++) {
|
||||||
|
await tester.runAsync(
|
||||||
|
() => Future<void>.delayed(const Duration(seconds: 1)));
|
||||||
|
await tester.pump();
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('stop')));
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 3));
|
||||||
|
|
||||||
|
// Read the stored ride back through the real provider graph. The app is
|
||||||
|
// uninstalled when the run ends, taking its database with it, so anything worth
|
||||||
|
// knowing has to be reported from inside the test.
|
||||||
|
final container = ProviderScope.containerOf(
|
||||||
|
tester.element(find.byType(MaterialApp)),
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
|
final repo = container.read(tripRepositoryProvider);
|
||||||
|
final trips = await repo.watchCompletedTrips().first;
|
||||||
|
|
||||||
|
debugPrint('RIPPR-PROBE completedTrips=${trips.length}');
|
||||||
|
for (final t in trips) {
|
||||||
|
final points = await repo.pointsForTrip(t.id);
|
||||||
|
final segments = await repo.segmentsForTrip(t.id);
|
||||||
|
final lats = [for (final p in points) p.latitude];
|
||||||
|
debugPrint('RIPPR-PROBE trip=${t.id} points=${t.pointCount} '
|
||||||
|
'distanceM=${t.distanceM.toStringAsFixed(1)} '
|
||||||
|
'maxSpeedKmh=${t.maxSpeedKmh.toStringAsFixed(1)} '
|
||||||
|
'movingMs=${t.movingMillis} segments=${segments.length}');
|
||||||
|
if (lats.isNotEmpty) {
|
||||||
|
debugPrint('RIPPR-PROBE latSpan=${(lats.reduce((a, b) => a > b ? a : b) - lats.reduce((a, b) => a < b ? a : b)).toStringAsFixed(5)}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// What CAN be proven locally: the ride persisted, moved, and is listed.
|
||||||
|
expect(trips, isNotEmpty, reason: 'the ride was not saved');
|
||||||
|
expect(trips.first.pointCount, greaterThan(0));
|
||||||
|
expect(trips.first.distanceM, greaterThan(0),
|
||||||
|
reason: 'the simulator moved, so distance must be non-zero');
|
||||||
|
|
||||||
|
// Now the list, then the detail screen with its map.
|
||||||
|
await tester.tap(find.text('Rides'));
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 2));
|
||||||
|
expect(find.byType(Card), findsWidgets, reason: 'the ride should be listed');
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(Key('trip-${trips.first.id}')));
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 4));
|
||||||
|
expect(find.text('DISTANCE'), findsOneWidget);
|
||||||
|
debugPrint('RIPPR-PROBE detail screen rendered');
|
||||||
|
|
||||||
|
// Hold here so an external `simctl io screenshot` can capture the detail screen and
|
||||||
|
// its map. Tapping the iOS simulator is not scriptable, so this is how a visual
|
||||||
|
// check gets taken at all.
|
||||||
|
for (var i = 0; i < 14; i++) {
|
||||||
|
await tester.runAsync(
|
||||||
|
() => Future<void>.delayed(const Duration(seconds: 1)));
|
||||||
|
await tester.pump();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user