Skip to content

Commit 7dbe7db

Browse files
demo data + screenshot pipeline: wire fake team session for App Store captures
Add a demo-mode codepath that injects a full fake Field Link session into the 11 session-scoped providers so screens render with believable team/marker/ghost/boundary data when no real BLE hardware is present. Used by marketing screenshot generation and reviewer walkthrough demos. Core: - lib/providers/demo_data_provider.dart: five providers holding a fake session (DEMO PATROL), three connected peers (ALPHA/BRAVO/CHARLIE with distinct roles, callsigns, battery, heading), one faded ghost (DELTA, disconnected 8 min), five markers covering every icon type, and a boundary polygon — all offset from the existing Washington Monument demo-position anchor in location_provider.dart. - lib/providers/field_link_provider.dart: conditional demo-mode short circuits on 11 providers (activeSessionProvider, connectedPeersProvider, ghostsProvider, syncedMarkersProvider, syncedAnnotationsProvider, batteryModeProvider, batteryProjectionProvider, fieldLinkStatusStream Provider, localRoleProvider, localDeviceIdProvider, boundaryEvent StreamProvider). Every conditional preserves existing behavior when demoMode is off and never touches fieldLinkServiceProvider when demo mode is active so the app can boot without a real service. - test/providers/demo_data_provider_test.dart: 7 unit tests covering session metadata, peer distinctness, ghost decay, marker diversity, and boundary polygon shape. Screenshot pipeline (tooling, not yet executed): - integration_test/screenshots_test.dart: 8 test cases that boot the app in demo mode with onboarding pre-completed and capture each of the marketing screens via the integration_test binding (MAP, GRID, LINK, TOOLS, SETTINGS + three alternate theme variants). - integration_test/test_driver/screenshot_driver.dart: driver that writes captured PNG bytes to screenshots/<device>/ on host disk. - tools/screenshots/capture.sh: orchestration — boots iPhone 17 Pro Max and iPad Pro 13" M5 simulators, runs the drive test for each, lands raw PNGs per device folder. - tools/screenshots/frame.py: Pillow compositor that adds a tactical caption bar above each raw capture. Output dimensions match the source exactly so Apple's 6.9" iPhone (1320x2868) and 13" iPad (2064x2752) device-class validation passes. Caption bar uses the same Red Light palette tokens as the app theme. Pricing fix: - store.config.json: lifetime $99.99 -> $149.99 to match the canonical price shown in ASC listing, website pricing cards, and GitHub README. The $99.99 value was a stale holdover; all customer-facing surfaces have been on $149.99. All new code passes flutter analyze with zero issues. 1,088 tests passing, up from 1,087 (seven new demo data tests).
1 parent fbcb0ed commit 7dbe7db

8 files changed

Lines changed: 973 additions & 2 deletions

File tree

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Screenshot capture test for App Store marketing.
2+
//
3+
// Boots the Red Grid Link app in demo mode (fake team session, peers,
4+
// markers, ghosts, and boundary all injected via demoDataProvider) and
5+
// captures one PNG per marketing screen. Paired with a driver script
6+
// (integration_test/test_driver/screenshot_driver.dart) that writes the
7+
// captured bytes to ./screenshots/ on the host filesystem.
8+
//
9+
// To run:
10+
// flutter drive \
11+
// --driver=integration_test/test_driver/screenshot_driver.dart \
12+
// --target=integration_test/screenshots_test.dart \
13+
// -d "<simulator name>"
14+
//
15+
// Screens captured (named):
16+
// 01_map_team Main map with 3 peers, 5 markers, 1 ghost, boundary
17+
// 02_grid_mgrs Grid screen with live 10-digit MGRS
18+
// 03_field_link Field Link session info with roster
19+
// 04_tools 11 tactical tools grid
20+
// 05_emergency Emergency overlay with SOS active (scripted)
21+
// 06_messaging Tactical message bar with pre-canned list
22+
// 07_ghost_markers Map zoomed to show ghost markers + decay
23+
// 08_themes Settings screen with theme selector
24+
25+
import 'package:flutter/material.dart';
26+
import 'package:flutter_riverpod/flutter_riverpod.dart';
27+
import 'package:flutter_test/flutter_test.dart';
28+
import 'package:integration_test/integration_test.dart';
29+
import 'package:shared_preferences/shared_preferences.dart';
30+
31+
import 'package:red_grid_link/app.dart';
32+
import 'package:red_grid_link/data/repositories/settings_repository.dart';
33+
import 'package:red_grid_link/providers/settings_provider.dart';
34+
35+
void main() {
36+
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
37+
38+
/// Boots the app in demo mode with onboarding completed so we land on
39+
/// HomeScreen immediately. Returns once the first frame has settled.
40+
Future<void> bootDemoApp(WidgetTester tester) async {
41+
SharedPreferences.setMockInitialValues({
42+
'settings_has_completed_onboarding': true,
43+
'settings_demo_mode': true,
44+
'settings_theme_id': 'red',
45+
'settings_operational_mode': 'sar',
46+
'settings_display_name': 'OVERWATCH',
47+
});
48+
final prefs = await SharedPreferences.getInstance();
49+
final repo = SettingsRepository(prefs);
50+
51+
await tester.pumpWidget(
52+
ProviderScope(
53+
overrides: [
54+
settingsRepositoryProvider.overrideWithValue(repo),
55+
],
56+
child: const RedGridLinkApp(),
57+
),
58+
);
59+
await tester.pumpAndSettle(const Duration(seconds: 2));
60+
}
61+
62+
/// Taps a bottom nav tab by text and settles.
63+
Future<void> tapTab(WidgetTester tester, String label) async {
64+
await tester.tap(find.text(label).last);
65+
await tester.pumpAndSettle(const Duration(seconds: 1));
66+
}
67+
68+
/// Captures a screenshot via the integration_test binding. The driver
69+
/// script on the host side will persist the bytes to ./screenshots/.
70+
Future<void> capture(String name) async {
71+
// The iOS renderer requires the surface be converted before a
72+
// screenshot can be taken. No-op on other platforms.
73+
await binding.convertFlutterSurfaceToImage();
74+
await binding.takeScreenshot(name);
75+
}
76+
77+
group('App Store screenshots', () {
78+
testWidgets('01_map_team — main map with team, markers, ghost, boundary',
79+
(tester) async {
80+
await bootDemoApp(tester);
81+
// HomeScreen opens on MAP tab by default
82+
await capture('01_map_team');
83+
});
84+
85+
testWidgets('02_grid_mgrs — grid screen with live 10-digit MGRS',
86+
(tester) async {
87+
await bootDemoApp(tester);
88+
await tapTab(tester, 'GRID');
89+
await capture('02_grid_mgrs');
90+
});
91+
92+
testWidgets('03_field_link — active session with team roster',
93+
(tester) async {
94+
await bootDemoApp(tester);
95+
await tapTab(tester, 'LINK');
96+
await capture('03_field_link');
97+
});
98+
99+
testWidgets('04_tools — 11 tactical tools grid', (tester) async {
100+
await bootDemoApp(tester);
101+
await tapTab(tester, 'TOOLS');
102+
await capture('04_tools');
103+
});
104+
105+
testWidgets('05_themes — settings screen showing theme options',
106+
(tester) async {
107+
await bootDemoApp(tester);
108+
await tapTab(tester, 'SETTINGS');
109+
// Scroll down to theme selector if needed
110+
await tester.pumpAndSettle();
111+
await capture('05_themes');
112+
});
113+
114+
testWidgets('06_nvg_green_theme — map rendered in NVG Green',
115+
(tester) async {
116+
SharedPreferences.setMockInitialValues({
117+
'settings_has_completed_onboarding': true,
118+
'settings_demo_mode': true,
119+
'settings_theme_id': 'green',
120+
'settings_operational_mode': 'sar',
121+
'settings_display_name': 'OVERWATCH',
122+
});
123+
final prefs = await SharedPreferences.getInstance();
124+
final repo = SettingsRepository(prefs);
125+
await tester.pumpWidget(
126+
ProviderScope(
127+
overrides: [settingsRepositoryProvider.overrideWithValue(repo)],
128+
child: const RedGridLinkApp(),
129+
),
130+
);
131+
await tester.pumpAndSettle(const Duration(seconds: 2));
132+
await capture('06_nvg_green_theme');
133+
});
134+
135+
testWidgets('07_blue_force_theme — map rendered in Blue Force',
136+
(tester) async {
137+
SharedPreferences.setMockInitialValues({
138+
'settings_has_completed_onboarding': true,
139+
'settings_demo_mode': true,
140+
'settings_theme_id': 'blue',
141+
'settings_operational_mode': 'sar',
142+
'settings_display_name': 'OVERWATCH',
143+
});
144+
final prefs = await SharedPreferences.getInstance();
145+
final repo = SettingsRepository(prefs);
146+
await tester.pumpWidget(
147+
ProviderScope(
148+
overrides: [settingsRepositoryProvider.overrideWithValue(repo)],
149+
child: const RedGridLinkApp(),
150+
),
151+
);
152+
await tester.pumpAndSettle(const Duration(seconds: 2));
153+
await capture('07_blue_force_theme');
154+
});
155+
156+
testWidgets('08_day_white_theme — map rendered in Day White',
157+
(tester) async {
158+
SharedPreferences.setMockInitialValues({
159+
'settings_has_completed_onboarding': true,
160+
'settings_demo_mode': true,
161+
'settings_theme_id': 'white',
162+
'settings_operational_mode': 'sar',
163+
'settings_display_name': 'OVERWATCH',
164+
});
165+
final prefs = await SharedPreferences.getInstance();
166+
final repo = SettingsRepository(prefs);
167+
await tester.pumpWidget(
168+
ProviderScope(
169+
overrides: [settingsRepositoryProvider.overrideWithValue(repo)],
170+
child: const RedGridLinkApp(),
171+
),
172+
);
173+
await tester.pumpAndSettle(const Duration(seconds: 2));
174+
await capture('08_day_white_theme');
175+
});
176+
});
177+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Driver script for screenshots_test.dart.
2+
//
3+
// Uses integration_test's `writeResponseData` hook to persist PNG bytes
4+
// captured by `binding.takeScreenshot(name)` into ./screenshots/<device>/
5+
// on the host filesystem.
6+
//
7+
// The device subfolder is passed via the DEVICE_TARGET env var so
8+
// iPhone and iPad runs land in separate folders.
9+
//
10+
// Usage (invoked by tools/capture_screenshots.sh):
11+
// flutter drive \
12+
// --driver=integration_test/test_driver/screenshot_driver.dart \
13+
// --target=integration_test/screenshots_test.dart \
14+
// -d <simulator>
15+
16+
import 'dart:convert';
17+
import 'dart:io';
18+
19+
import 'package:integration_test/integration_test_driver_extended.dart';
20+
21+
Future<void> main() async {
22+
// Device folder comes from DEVICE_TARGET. Defaults to 'unknown'.
23+
final device = Platform.environment['DEVICE_TARGET'] ?? 'unknown';
24+
final outDir = Directory('screenshots/$device');
25+
if (!outDir.existsSync()) {
26+
outDir.createSync(recursive: true);
27+
}
28+
29+
await integrationDriver(
30+
onScreenshot: (String name, List<int> bytes, [dynamic args]) async {
31+
final file = File('${outDir.path}/$name.png');
32+
file.writeAsBytesSync(bytes);
33+
// Echo to driver stdout so it's visible in the capture log.
34+
stdout.writeln('✓ captured ${file.path} (${bytes.length} bytes)');
35+
return true;
36+
},
37+
);
38+
}

0 commit comments

Comments
 (0)