Drops the app bar, moves settings onto the map with the other controls, and takes the attribution bar off the main screen. The app bar held only the app name — which the launcher already shows — and one action, so removing it hands its height to the map rather than leaving an empty strip. The settings button joins search and recentre in the column over the map, where the rest of the controls that act on the map already live. The attribution needed checking before it could move, because it is a licence obligation rather than a layout choice. The OSMF guidelines settle it: for a browsable map the credit does not have to be permanently visible, provided "the user must still be able to find the licence information if they look for it, for example from an '(i)' button in the corner of the map or an 'About' option in a menu". Map to settings to "Fonti e licenze" is exactly that, and MapLibre's own (i) control now sits in the map's top-right corner as a second route — it had been landing underneath the new button column. Two things had to follow the bar rather than disappear with it. The radar manifest carries its own credit for the frames on screen, set by whoever published them and absent from the region config, so the Sources screen now shows it alongside the configured attributions. And the settings entry that leads there carries a subtitle saying what it holds, because "Fonti e licenze" alone does not tell a reader that this is where the licence information went. settings_screen_test asserts that entry exists and opens the screen. Removing it would be a licence breach, so it should fail the build rather than ship quietly. Verified on the emulator: no title bar, the map runs to the top with the demo notice above it, the three controls stack bottom-right without colliding with the (i), and settings shows the sources entry with its subtitle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
116 lines
3.8 KiB
Dart
116 lines
3.8 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:nuvolari/core/region/geo.dart';
|
|
import 'package:nuvolari/core/region/region_config.dart';
|
|
import 'package:nuvolari/core/region/region_repository.dart';
|
|
import 'package:nuvolari/features/places/location_service.dart';
|
|
import 'package:nuvolari/features/places/places_repository.dart';
|
|
import 'package:nuvolari/features/places/saved_places.dart';
|
|
import 'package:nuvolari/features/settings/settings_screen.dart';
|
|
import 'package:nuvolari/features/sources/sources_screen.dart';
|
|
import 'package:nuvolari/l10n/app_localizations.dart';
|
|
|
|
/// Answers without touching the platform channels geolocator needs.
|
|
class FakeLocationService implements LocationService {
|
|
FakeLocationService(this.state);
|
|
|
|
final LocationAvailability state;
|
|
|
|
@override
|
|
Future<LocationAvailability> availability() async => state;
|
|
|
|
@override
|
|
Future<LocationAvailability> request() async => state;
|
|
|
|
@override
|
|
Future<GeoPoint?> currentPoint() async => const GeoPoint(7.686, 45.070);
|
|
}
|
|
|
|
Widget wrap(
|
|
RegionConfig region, {
|
|
LocationAvailability availability = LocationAvailability.denied,
|
|
}) => ProviderScope(
|
|
overrides: [
|
|
regionConfigProvider.overrideWith((ref) async => region),
|
|
placesStoreProvider.overrideWithValue(InMemoryPlacesStore()),
|
|
locationServiceProvider.overrideWithValue(
|
|
FakeLocationService(availability),
|
|
),
|
|
],
|
|
child: MaterialApp(
|
|
locale: const Locale('it'),
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: const SettingsScreen(),
|
|
),
|
|
);
|
|
|
|
void main() {
|
|
late RegionConfig region;
|
|
|
|
setUpAll(() {
|
|
region = RegionConfig.parse(
|
|
File('assets/regions/piemonte.json').readAsStringSync(),
|
|
);
|
|
});
|
|
|
|
group('SettingsScreen', () {
|
|
// The map carries no permanent credit line any more, so this entry is the
|
|
// route the OSM attribution guidelines require. Losing it would be a
|
|
// licence breach, not a cosmetic regression, so it is asserted here.
|
|
testWidgets('offers the sources and licences entry', (tester) async {
|
|
await tester.pumpWidget(wrap(region));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Fonti e licenze'), findsOneWidget);
|
|
expect(
|
|
find.text('Licenze e attribuzioni dei dati mostrati'),
|
|
findsOneWidget,
|
|
);
|
|
});
|
|
|
|
testWidgets('opens the Sources screen from that entry', (tester) async {
|
|
await tester.pumpWidget(wrap(region));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('Fonti e licenze'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(SourcesScreen), findsOneWidget);
|
|
expect(find.text('App non ufficiale'), findsOneWidget);
|
|
});
|
|
|
|
// The disclosure has to be readable after the fact, not only in the moment
|
|
// the system dialog is about to appear.
|
|
testWidgets('repeats the location disclosure', (tester) async {
|
|
await tester.pumpWidget(wrap(region));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.textContaining('resta sul dispositivo'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('reports the permission state', (tester) async {
|
|
await tester.pumpWidget(
|
|
wrap(region, availability: LocationAvailability.granted),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Permesso concesso'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('sends the user to system settings when refused for good', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
wrap(region, availability: LocationAvailability.deniedForever),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Apri impostazioni di sistema'), findsOneWidget);
|
|
});
|
|
});
|
|
}
|