Built together because V3-02's settings screen needed something real for V3-03's units to control -- executed in reverse of the ticket numbering, but both tickets are independently complete. V3-03: UnitSystem (metric/imperial) lives in domain/models.dart alongside Activity, defaulting from Platform.localeName on first launch. Storage stays SI everywhere -- conversion happens only in ui/format.dart, at the last possible moment, which is now documented as the file's central invariant. Threaded through all three screens plus the speed histogram's bucket labels, which relabel for display without changing how speedHistogram itself bins. Export was deliberately left untouched: gpx()/geoJson() take no UnitSystem parameter at all, a stronger guarantee than validating one would be. V3-02: SettingsScreen reachable from the record screen. Map render toggle (nothing previously exposed mapEnabledProvider to the user, despite it existing since T15 -- there was nothing to "move off trip detail" as drafted), unit selector, upload endpoint with http(s) validation, a copyable device id, and a licences page via Flutter's built-in showLicensePage. Skipped package_info_plus (static version string instead) and a privacy-policy link (none published yet) as disproportionate to an S-sized ticket. Neither ticket needed the ConfigNotifier the implementation notes proposed: unitSystemProvider reuses the exact StateProvider-seeded-from-Config pattern mapEnabledProvider already established, since Config mutates its own backing SharedPreferences in place and re-assigning the same instance would never notify a watcher anyway. A real locale-dependent flake was caught, not just anticipated: a test asserting a fresh Config defaults to metric failed, because this machine's own locale resolves to a region in the imperial set. Fixed by seeding an explicit value before asserting, and config_test.dart's locale test was written from the start to only prove the fallback path doesn't throw, not to assert which value it returns. 221 tests (211 -> 221): 13 format_test, 9 config_test, 9 settings_screen_test, 1 confirming the record screen's settings button is genuinely wired. Analyze clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
88 lines
3.0 KiB
Dart
88 lines
3.0 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:rippr/src/config/config.dart';
|
|
import 'package:rippr/src/domain/models.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
/// `Config` had no dedicated tests before V3-03 — every existing widget test leaves
|
|
/// `configProvider` null, so this is its first direct coverage.
|
|
void main() {
|
|
Future<Config> freshConfig([Map<String, Object> initial = const {}]) async {
|
|
SharedPreferences.setMockInitialValues(initial);
|
|
return Config.load();
|
|
}
|
|
|
|
group('unitSystem', () {
|
|
test('an explicit choice always wins over the locale default', () async {
|
|
final config = await freshConfig();
|
|
await config.setUnitSystem(UnitSystem.imperial);
|
|
expect(config.unitSystem, UnitSystem.imperial);
|
|
|
|
await config.setUnitSystem(UnitSystem.metric);
|
|
expect(config.unitSystem, UnitSystem.metric);
|
|
});
|
|
|
|
test('with nothing stored, falls back to a locale default without throwing', () async {
|
|
// The exact value is environment-dependent (it reads the real platform locale),
|
|
// so this asserts the fallback path is safe to reach, not a specific answer.
|
|
final config = await freshConfig();
|
|
expect(config.unitSystem, isA<UnitSystem>());
|
|
});
|
|
});
|
|
|
|
group('mapEnabled', () {
|
|
test('defaults to true', () async {
|
|
final config = await freshConfig();
|
|
expect(config.mapEnabled, isTrue);
|
|
});
|
|
|
|
test('round-trips through set/get', () async {
|
|
final config = await freshConfig();
|
|
await config.setMapEnabled(false);
|
|
expect(config.mapEnabled, isFalse);
|
|
});
|
|
});
|
|
|
|
group('uploadEndpoint', () {
|
|
test('defaults to empty, not null or a placeholder', () async {
|
|
final config = await freshConfig();
|
|
expect(config.uploadEndpoint, '');
|
|
});
|
|
|
|
test('trims whitespace on write', () async {
|
|
final config = await freshConfig();
|
|
await config.setUploadEndpoint(' https://example.test/ingest ');
|
|
expect(config.uploadEndpoint, 'https://example.test/ingest');
|
|
});
|
|
});
|
|
|
|
group('deviceId', () {
|
|
test('is generated once and then stable across reads', () async {
|
|
final config = await freshConfig();
|
|
final first = config.deviceId;
|
|
final second = config.deviceId;
|
|
expect(first, second);
|
|
expect(first, isNotEmpty);
|
|
});
|
|
|
|
test('is a v4 UUID', () async {
|
|
final config = await freshConfig();
|
|
// xxxxxxxx-xxxx-4xxx-{8,9,a,b}xxx-xxxxxxxxxxxx
|
|
final v4 = RegExp(
|
|
r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
|
|
);
|
|
expect(v4.hasMatch(config.deviceId), isTrue,
|
|
reason: 'got ${config.deviceId}');
|
|
});
|
|
|
|
test('persists across a fresh Config over the same preferences', () async {
|
|
SharedPreferences.setMockInitialValues({});
|
|
final first = await Config.load();
|
|
final id = first.deviceId;
|
|
|
|
final second = await Config.load();
|
|
expect(second.deviceId, id,
|
|
reason: 'a server distinguishing riders needs this to be stable');
|
|
});
|
|
});
|
|
}
|