import 'dart:convert'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:nuvolari/core/region/region_config.dart'; import 'package:nuvolari/features/map/map_style.dart'; void main() { late RegionConfig region; setUpAll(() { region = RegionConfig.parse( File('assets/regions/piemonte.json').readAsStringSync(), ); }); group('MapStyle.buildFallbackStyle', () { late Map style; setUp(() { style = jsonDecode( MapStyle.buildFallbackStyle(region), ) as Map; }); test('is a valid MapLibre style document', () { expect(style['version'], 8); expect(style['sources'], isA>()); expect(style['layers'], isA>()); }); // The whole point of the fallback is that it works with no network at all, // so a style that referenced a tile server would defeat it. test('references no network sources', () { final encoded = jsonEncode(style); expect(encoded, isNot(contains('http://'))); expect(encoded, isNot(contains('https://'))); final sources = style['sources']! as Map; for (final source in sources.values) { expect((source! as Map)['type'], 'geojson'); } }); test('draws the region extent as a closed ring', () { final source = (style['sources']! as Map)['region-extent']! as Map; final geometry = (source['data']! as Map)['geometry']! as Map; final ring = (geometry['coordinates']! as List).first! as List; expect(geometry['type'], 'Polygon'); // Five points: four corners plus the repeated first point that closes it. expect(ring, hasLength(5)); expect(ring.first, equals(ring.last)); }); test('the ring matches the region bounding box', () { final source = (style['sources']! as Map)['region-extent']! as Map; final geometry = (source['data']! as Map)['geometry']! as Map; final ring = (geometry['coordinates']! as List).first! as List; final longitudes = []; final latitudes = []; for (final point in ring) { final pair = point! as List; longitudes.add((pair[0]! as num).toDouble()); latitudes.add((pair[1]! as num).toDouble()); } expect(longitudes.reduce((a, b) => a < b ? a : b), region.bounds.west); expect(longitudes.reduce((a, b) => a > b ? a : b), region.bounds.east); expect(latitudes.reduce((a, b) => a < b ? a : b), region.bounds.south); expect(latitudes.reduce((a, b) => a > b ? a : b), region.bounds.north); }); test('has a background layer beneath the region layers', () { final layers = (style['layers']! as List) .map((layer) => (layer! as Map)['id']) .toList(); expect(layers.first, 'background'); expect(layers, contains('region-extent-fill')); expect(layers, contains('region-extent-outline')); }); }); group('MapStyle.forRegion', () { // The test binary is built with no --dart-define, so MAP_STYLE_URL is empty // and the free key-less base map is what must come back. test('defaults to OpenFreeMap when nothing is configured', () { final style = MapStyle.forRegion(region); expect(style.kind, BaseMapKind.openFreeMap); expect(style.styleString, MapStyle.openFreeMapStyleUrl); expect(style.styleString, startsWith('https://')); }); // OpenMapTiles and OpenStreetMap are mandatory credits for these tiles. // OpenFreeMap's own credit is optional by their terms and lives on the // Sources screen instead of the always-visible bar. test('the default base map owes the OpenMapTiles and OSM credits', () { final ids = MapStyle.forRegion(region).attributionIds; expect(ids, containsAll(['openmaptiles', 'osm'])); expect(ids, isNot(contains('openfreemap'))); }); test('a custom style URL owes the same credits', () { const custom = MapStyle( kind: BaseMapKind.custom, styleString: 'https://example.invalid/style.json', ); expect( custom.attributionIds, containsAll(['openmaptiles', 'osm']), ); }); // Crediting OpenStreetMap while showing a style that contains no OSM data // would be a false attribution, so the fallback owes nothing. test('the offline fallback claims no base map attribution', () { final fallback = MapStyle( kind: BaseMapKind.offlineFallback, styleString: MapStyle.buildFallbackStyle(region), ); expect(fallback.attributionIds, isEmpty); expect(fallback.styleString, startsWith('{')); }); }); }