Files
deflock-app/test/services/deflock_tile_provider_test.dart
T
Doug BorgandClaude Opus 4.6 8983939b05 Delegate network tile fetching to NetworkTileProvider
Replace our custom tile pipeline (fetchRemoteTile / _SimpleSemaphore /
exponential backoff) with flutter_map's built-in NetworkTileProvider,
gaining persistent disk cache, ETag revalidation, RetryClient, and
obsolete request aborting for free.

DeflockTileProvider now extends NetworkTileProvider and overrides
getTileUrl() to route through TileType.getTileUrl() (quadkey,
subdomains, API keys). getImageWithCancelLoadingSupport() routes
between two paths at runtime: the common network path (super) when
no offline areas exist, and a DeflockOfflineTileImageProvider for
offline-first when they do.

- Delete tiles_from_remote.dart (semaphore, retry loop, spatial helpers)
- Simplify MapDataProvider._fetchRemoteTileFromCurrentProvider to plain
  http.get (only used by offline area downloader now)
- Remove dead clearTileQueue/clearTileQueueSelective from MapDataProvider
- Remove 7 tile fetch constants from dev_config.dart
- TileLayerManager now disposes provider on cache clear and uses actual
  urlTemplate for cache key generation
- 9 new tests covering URL delegation, routing, and equality

Closes #87 Phase 2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 20:07:56 -07:00

252 lines
8.0 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
import 'package:deflockapp/app_state.dart';
import 'package:deflockapp/models/tile_provider.dart' as models;
import 'package:deflockapp/services/deflock_tile_provider.dart';
class MockAppState extends Mock implements AppState {}
void main() {
late DeflockTileProvider provider;
late MockAppState mockAppState;
setUp(() {
mockAppState = MockAppState();
AppState.instance = mockAppState;
// Default stubs: online, OSM provider selected, no offline areas
when(() => mockAppState.offlineMode).thenReturn(false);
when(() => mockAppState.selectedTileProvider).thenReturn(
const models.TileProvider(
id: 'openstreetmap',
name: 'OpenStreetMap',
tileTypes: [],
),
);
when(() => mockAppState.selectedTileType).thenReturn(
const models.TileType(
id: 'osm_street',
name: 'Street Map',
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '© OpenStreetMap',
maxZoom: 19,
),
);
provider = DeflockTileProvider();
});
tearDown(() async {
await provider.dispose();
AppState.instance = MockAppState();
});
group('DeflockTileProvider', () {
test('supportsCancelLoading is true', () {
expect(provider.supportsCancelLoading, isTrue);
});
test('getTileUrl() delegates to TileType.getTileUrl()', () {
const coords = TileCoordinates(1, 2, 3);
final options = TileLayer(urlTemplate: 'ignored/{z}/{x}/{y}');
final url = provider.getTileUrl(coords, options);
expect(url, equals('https://tile.openstreetmap.org/3/1/2.png'));
});
test('getTileUrl() includes API key when present', () {
when(() => mockAppState.selectedTileProvider).thenReturn(
const models.TileProvider(
id: 'mapbox',
name: 'Mapbox',
apiKey: 'test_key_123',
tileTypes: [],
),
);
when(() => mockAppState.selectedTileType).thenReturn(
const models.TileType(
id: 'mapbox_satellite',
name: 'Satellite',
urlTemplate:
'https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}@2x.jpg90?access_token={api_key}',
attribution: '© Mapbox',
),
);
const coords = TileCoordinates(1, 2, 10);
final options = TileLayer(urlTemplate: 'ignored');
final url = provider.getTileUrl(coords, options);
expect(url, contains('access_token=test_key_123'));
expect(url, contains('/10/1/2@2x'));
});
test('getTileUrl() falls back to super when no provider selected', () {
when(() => mockAppState.selectedTileProvider).thenReturn(null);
when(() => mockAppState.selectedTileType).thenReturn(null);
const coords = TileCoordinates(1, 2, 3);
final options = TileLayer(urlTemplate: 'https://example.com/{z}/{x}/{y}');
final url = provider.getTileUrl(coords, options);
// Super implementation uses the urlTemplate from TileLayer options
expect(url, equals('https://example.com/3/1/2'));
});
test('routes to network path when no offline areas exist', () {
// offlineMode = false, OfflineAreaService not initialized → no offline areas
const coords = TileCoordinates(5, 10, 12);
final options = TileLayer(urlTemplate: 'test/{z}/{x}/{y}');
final cancelLoading = Future<void>.value();
final imageProvider = provider.getImageWithCancelLoadingSupport(
coords,
options,
cancelLoading,
);
// Should NOT be a DeflockOfflineTileImageProvider — it should be the
// NetworkTileImageProvider returned by super
expect(imageProvider, isNot(isA<DeflockOfflineTileImageProvider>()));
});
test('routes to offline path when offline mode is enabled', () {
when(() => mockAppState.offlineMode).thenReturn(true);
const coords = TileCoordinates(5, 10, 12);
final options = TileLayer(urlTemplate: 'test/{z}/{x}/{y}');
final cancelLoading = Future<void>.value();
final imageProvider = provider.getImageWithCancelLoadingSupport(
coords,
options,
cancelLoading,
);
expect(imageProvider, isA<DeflockOfflineTileImageProvider>());
final offlineProvider = imageProvider as DeflockOfflineTileImageProvider;
expect(offlineProvider.isOfflineOnly, isTrue);
expect(offlineProvider.coordinates, equals(coords));
expect(offlineProvider.providerId, equals('openstreetmap'));
expect(offlineProvider.tileTypeId, equals('osm_street'));
});
});
group('DeflockOfflineTileImageProvider', () {
test('equal for same coordinates and provider/type', () {
const coords = TileCoordinates(1, 2, 3);
final options = TileLayer(urlTemplate: 'test/{z}/{x}/{y}');
final cancel = Future<void>.value();
final a = DeflockOfflineTileImageProvider(
coordinates: coords,
options: options,
httpClient: http.Client(),
headers: const {},
cancelLoading: cancel,
isOfflineOnly: false,
providerId: 'prov_a',
tileTypeId: 'type_1',
tileUrl: 'https://example.com/3/1/2',
);
final b = DeflockOfflineTileImageProvider(
coordinates: coords,
options: options,
httpClient: http.Client(),
headers: const {},
cancelLoading: cancel,
isOfflineOnly: true, // different — but not in ==
providerId: 'prov_a',
tileTypeId: 'type_1',
tileUrl: 'https://other.com/3/1/2', // different — but not in ==
);
expect(a, equals(b));
expect(a.hashCode, equals(b.hashCode));
});
test('not equal for different coordinates', () {
const coords1 = TileCoordinates(1, 2, 3);
const coords2 = TileCoordinates(1, 2, 4);
final options = TileLayer(urlTemplate: 'test/{z}/{x}/{y}');
final cancel = Future<void>.value();
final a = DeflockOfflineTileImageProvider(
coordinates: coords1,
options: options,
httpClient: http.Client(),
headers: const {},
cancelLoading: cancel,
isOfflineOnly: false,
providerId: 'prov_a',
tileTypeId: 'type_1',
tileUrl: 'url1',
);
final b = DeflockOfflineTileImageProvider(
coordinates: coords2,
options: options,
httpClient: http.Client(),
headers: const {},
cancelLoading: cancel,
isOfflineOnly: false,
providerId: 'prov_a',
tileTypeId: 'type_1',
tileUrl: 'url2',
);
expect(a, isNot(equals(b)));
});
test('not equal for different provider or type', () {
const coords = TileCoordinates(1, 2, 3);
final options = TileLayer(urlTemplate: 'test/{z}/{x}/{y}');
final cancel = Future<void>.value();
final base = DeflockOfflineTileImageProvider(
coordinates: coords,
options: options,
httpClient: http.Client(),
headers: const {},
cancelLoading: cancel,
isOfflineOnly: false,
providerId: 'prov_a',
tileTypeId: 'type_1',
tileUrl: 'url',
);
final diffProvider = DeflockOfflineTileImageProvider(
coordinates: coords,
options: options,
httpClient: http.Client(),
headers: const {},
cancelLoading: cancel,
isOfflineOnly: false,
providerId: 'prov_b',
tileTypeId: 'type_1',
tileUrl: 'url',
);
final diffType = DeflockOfflineTileImageProvider(
coordinates: coords,
options: options,
httpClient: http.Client(),
headers: const {},
cancelLoading: cancel,
isOfflineOnly: false,
providerId: 'prov_a',
tileTypeId: 'type_2',
tileUrl: 'url',
);
expect(base, isNot(equals(diffProvider)));
expect(base.hashCode, isNot(equals(diffProvider.hashCode)));
expect(base, isNot(equals(diffType)));
expect(base.hashCode, isNot(equals(diffType.hashCode)));
});
});
}