import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:rippr/src/tiles/tile_cache.dart'; import 'package:rippr/src/tiles/tile_downloader.dart'; import 'package:rippr/src/tiles/tile_math.dart'; void main() { late Directory tempDir; late FileTileCache cache; setUp(() async { tempDir = await Directory.systemTemp.createTemp('rippr_tile_downloader_test_'); cache = FileTileCache(directory: tempDir, maxBytes: 1024 * 1024); }); tearDown(() async { if (tempDir.existsSync()) tempDir.deleteSync(recursive: true); }); List tiles(int n) => [for (var i = 0; i < n; i++) TileKey(1, i, 0)]; test('every tile is fetched exactly once and lands in the cache', () async { final fetched = []; final progress = await downloadTiles( tiles: tiles(5), cache: cache, fetchTile: (key) async { fetched.add(key); return Uint8List.fromList([1, 2, 3]); }, delay: Duration.zero, ).toList(); expect(fetched, tiles(5), reason: 'sequential, in order, no duplicates'); expect(progress.last, isA()); expect(progress.last.completed, 5); expect(progress.last.isDone, isTrue); for (final t in tiles(5)) { expect(await cache.get(t), isNotNull); } }); test('one failing tile does not abort the rest', () async { final progress = await downloadTiles( tiles: tiles(4), cache: cache, fetchTile: (key) async { if (key.x == 1) throw Exception('transient'); return Uint8List.fromList([1]); }, delay: Duration.zero, ).toList(); expect(progress.last.completed, 3); expect(progress.last.failed, 1); expect(progress.last.isDone, isTrue); expect(await cache.get(const TileKey(1, 1, 0)), isNull); expect(await cache.get(const TileKey(1, 0, 0)), isNotNull); expect(await cache.get(const TileKey(1, 2, 0)), isNotNull); }); test('cancelling mid-download keeps everything already fetched', () async { final token = CancelToken(); final events = []; await for (final p in downloadTiles( tiles: tiles(10), cache: cache, fetchTile: (key) async => Uint8List.fromList([1]), cancelToken: token, delay: Duration.zero, )) { events.add(p); if (p.completed == 3) token.cancel(); } final last = events.last; expect(last.isDone, isFalse, reason: 'stopped early -- fewer tiles than the total were fetched'); expect(last.completed, lessThan(10)); // Everything fetched before cancellation must still be in the cache -- a // consistent partial cache, not rolled back to nothing. for (var i = 0; i < last.completed; i++) { expect(await cache.get(TileKey(1, i, 0)), isNotNull); } }); test('an empty tile list completes immediately with no fetch calls', () async { var calls = 0; final progress = await downloadTiles( tiles: const [], cache: cache, fetchTile: (key) async { calls++; return Uint8List(0); }, ).toList(); expect(calls, 0); expect(progress.last.isDone, isTrue); expect(progress.last.total, 0); }); }