import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:smtias_capture/providers/gallery_provider.dart'; import 'package:smtias_capture/services/raw_capture_service.dart'; /// テスト用 RawCaptureService.`scanFile` の呼び出しパスを記録する. class _FakeRawCaptureService extends RawCaptureService { final List scannedPaths = []; bool scanShouldThrow = false; @override Future scanFile(String path) async { if (scanShouldThrow) { throw Exception('テスト用 scanFile 失敗'); } scannedPaths.add(path); } } void main() { group('GalleryProvider.loadImages', () { late Directory tempDir; late GalleryProvider provider; setUp(() async { tempDir = await Directory.systemTemp.createTemp( 'smtias_capture_gallery_test_', ); provider = GalleryProvider(basePath: tempDir.path); }); tearDown(() async { if (await tempDir.exists()) { await tempDir.delete(recursive: true); } }); test('.png のみある場合,全件取得する', () async { await File('${tempDir.path}/MiniTIAS_20260101_120000.png').create(); await File('${tempDir.path}/MiniTIAS_20260101_120001.png').create(); await provider.loadImages(); final names = provider.images .map((f) => f.path.split(RegExp(r'[\\/]')).last) .toList(); expect(names, contains('MiniTIAS_20260101_120000.png')); expect(names, contains('MiniTIAS_20260101_120001.png')); expect(provider.images, hasLength(2)); }); test('.preview.jpg のみある場合,全件取得する', () async { await File( '${tempDir.path}/MiniTIAS_QM_20260101_120000.preview.jpg', ).create(); await File( '${tempDir.path}/MiniTIAS_QM_20260101_120001.preview.jpg', ).create(); await provider.loadImages(); final names = provider.images .map((f) => f.path.split(RegExp(r'[\\/]')).last) .toList(); expect(names, contains('MiniTIAS_QM_20260101_120000.preview.jpg')); expect(names, contains('MiniTIAS_QM_20260101_120001.preview.jpg')); expect(provider.images, hasLength(2)); }); test('.png と .preview.jpg が混在する場合,両方取得する', () async { await File('${tempDir.path}/MiniTIAS_20260101_120000.png').create(); await File( '${tempDir.path}/MiniTIAS_QM_20260101_120001.preview.jpg', ).create(); await provider.loadImages(); final names = provider.images .map((f) => f.path.split(RegExp(r'[\\/]')).last) .toList(); expect(names, contains('MiniTIAS_20260101_120000.png')); expect(names, contains('MiniTIAS_QM_20260101_120001.preview.jpg')); expect(provider.images, hasLength(2)); }); test('.dng や .meta.json は取得しない', () async { await File('${tempDir.path}/MiniTIAS_QM_20260101_120000.dng').create(); await File( '${tempDir.path}/MiniTIAS_QM_20260101_120000.meta.json', ).create(); await provider.loadImages(); expect(provider.images, isEmpty); }); test('通常 .jpg(.preview.jpg 以外)は取得しない', () async { // .preview.jpg のみが対象,単純な .jpg は除外される await File('${tempDir.path}/photo.jpg').create(); await File('${tempDir.path}/MiniTIAS_20260101_120000.jpg').create(); // 比較用に .preview.jpg を 1 件追加 await File( '${tempDir.path}/MiniTIAS_QM_20260101_120001.preview.jpg', ).create(); await provider.loadImages(); final names = provider.images .map((f) => f.path.split(RegExp(r'[\\/]')).last) .toList(); expect(names, isNot(contains('photo.jpg'))); expect(names, isNot(contains('MiniTIAS_20260101_120000.jpg'))); expect(names, contains('MiniTIAS_QM_20260101_120001.preview.jpg')); expect(provider.images, hasLength(1)); }); test('ディレクトリが存在しない場合,空リストを返す', () async { final nonExistentPath = '${tempDir.path}/nonexistent'; final providerNonExistent = GalleryProvider(basePath: nonExistentPath); await providerNonExistent.loadImages(); expect(providerNonExistent.images, isEmpty); }); test('ファイルが新しい順(更新日時降順)にソートされる', () async { // 先に古いファイルを作成し,十分な時間差を設けてから新しいファイルを作成する final olderPath = '${tempDir.path}/MiniTIAS_20260101_120000.png'; final newerPath = '${tempDir.path}/MiniTIAS_QM_20260101_120001.preview.jpg'; await File(olderPath).create(); // Windows NTFS の mtime 精度 (100ns) より十分大きい 1 秒以上待機 await Future.delayed(const Duration(seconds: 2)); await File(newerPath).create(); await provider.loadImages(); expect(provider.images, hasLength(2)); // 新しい順なので newer が先頭 final firstBasename = provider.images.first.path .split(RegExp(r'[\\/]')) .last; expect(firstBasename, 'MiniTIAS_QM_20260101_120001.preview.jpg'); }); }); group('GalleryProvider.deleteImage', () { late Directory tempDir; late _FakeRawCaptureService fakeRawCapture; late GalleryProvider provider; setUp(() async { tempDir = await Directory.systemTemp.createTemp( 'smtias_capture_del_test_', ); fakeRawCapture = _FakeRawCaptureService(); provider = GalleryProvider( basePath: tempDir.path, rawCaptureService: fakeRawCapture, ); }); tearDown(() async { if (await tempDir.exists()) { await tempDir.delete(recursive: true); } }); test('.preview.jpg を削除すると同 basename の .dng も削除される', () async { final previewPath = '${tempDir.path}/MiniTIAS_QM_20260101_120000.preview.jpg'; final dngFile = File('${tempDir.path}/MiniTIAS_QM_20260101_120000.dng'); final metaFile = File( '${tempDir.path}/MiniTIAS_QM_20260101_120000.meta.json', ); await File(previewPath).create(); await dngFile.create(); await metaFile.create(); await provider.loadImages(); // loadImages で取得した同一インスタンスを使って削除する final fileToDelete = provider.images.first; final result = await provider.deleteImage(fileToDelete); expect(result, isTrue); expect(await dngFile.exists(), isFalse); }); test('.preview.jpg を削除すると同 basename の .meta.json も削除される', () async { final previewPath = '${tempDir.path}/MiniTIAS_QM_20260101_120000.preview.jpg'; final dngFile = File('${tempDir.path}/MiniTIAS_QM_20260101_120000.dng'); final metaFile = File( '${tempDir.path}/MiniTIAS_QM_20260101_120000.meta.json', ); await File(previewPath).create(); await dngFile.create(); await metaFile.create(); await provider.loadImages(); final fileToDelete = provider.images.first; final result = await provider.deleteImage(fileToDelete); expect(result, isTrue); expect(await metaFile.exists(), isFalse); }); test('.png を削除しても他のファイルは削除されない', () async { final pngPath = '${tempDir.path}/MiniTIAS_20260101_120000.png'; // 同名の .dng が仮に存在しても,連動削除の対象にならない final dngFile = File('${tempDir.path}/MiniTIAS_20260101_120000.dng'); await File(pngPath).create(); await dngFile.create(); await provider.loadImages(); final fileToDelete = provider.images.first; final result = await provider.deleteImage(fileToDelete); expect(result, isTrue); // .dng は削除されていない expect(await dngFile.exists(), isTrue); }); test('連動削除対象の .dng が最初から存在しない場合でもエラーにならず .preview.jpg の削除は成功する', () async { final previewPath = '${tempDir.path}/MiniTIAS_QM_20260101_120000.preview.jpg'; final metaFile = File( '${tempDir.path}/MiniTIAS_QM_20260101_120000.meta.json', ); // .dng は作成しない await File(previewPath).create(); await metaFile.create(); await provider.loadImages(); final fileToDelete = provider.images.first; // .dng が存在しなくても例外にならない final result = await provider.deleteImage(fileToDelete); expect(result, isTrue); expect(await File(previewPath).exists(), isFalse); }); test('削除成功後,images リストから対象ファイルが除去される(.preview.jpg)', () async { final previewPath = '${tempDir.path}/MiniTIAS_QM_20260101_120000.preview.jpg'; await File(previewPath).create(); await provider.loadImages(); expect(provider.images, hasLength(1)); // loadImages で保持している同一インスタンスを使って削除する final fileToDelete = provider.images.first; final result = await provider.deleteImage(fileToDelete); expect(result, isTrue); expect(provider.images, isEmpty); }); test('削除成功後,images リストから対象ファイルが除去される(.png)', () async { final pngPath = '${tempDir.path}/MiniTIAS_20260101_120000.png'; await File(pngPath).create(); await provider.loadImages(); expect(provider.images, hasLength(1)); final fileToDelete = provider.images.first; final result = await provider.deleteImage(fileToDelete); expect(result, isTrue); expect(provider.images, isEmpty); }); }); // 本番の GalleryProvider を直接検証する.deleteImage は引数のファイルパスで // 動作し _basePath に依存しないため,一時ファイル + フェイクで実コードを exercise できる. group('GalleryProvider.deleteImage(本番実装・MediaStore 通知)', () { late Directory tempDir; late _FakeRawCaptureService fakeRawCapture; late GalleryProvider provider; setUp(() async { tempDir = await Directory.systemTemp.createTemp( 'smtias_capture_del_real_test_', ); fakeRawCapture = _FakeRawCaptureService(); provider = GalleryProvider(rawCaptureService: fakeRawCapture); }); tearDown(() async { if (await tempDir.exists()) { await tempDir.delete(recursive: true); } }); test( '.preview.jpg 削除後,削除した preview/.dng/.meta.json すべてのパスで scanFile が呼ばれる', () async { final previewPath = '${tempDir.path}/MiniTIAS_QM_20260101_120000.preview.jpg'; final dngPath = '${tempDir.path}/MiniTIAS_QM_20260101_120000.dng'; final metaPath = '${tempDir.path}/MiniTIAS_QM_20260101_120000.meta.json'; await File(previewPath).create(); await File(dngPath).create(); await File(metaPath).create(); final result = await provider.deleteImage(File(previewPath)); expect(result, isTrue); expect( fakeRawCapture.scannedPaths, containsAll([previewPath, dngPath, metaPath]), ); expect(fakeRawCapture.scannedPaths, hasLength(3)); }, ); test('存在しない連動ファイル(.dng なし)は scanFile 対象に含まれない', () async { final previewPath = '${tempDir.path}/MiniTIAS_QM_20260101_120000.preview.jpg'; final metaPath = '${tempDir.path}/MiniTIAS_QM_20260101_120000.meta.json'; // .dng は作成しない await File(previewPath).create(); await File(metaPath).create(); final result = await provider.deleteImage(File(previewPath)); expect(result, isTrue); expect(fakeRawCapture.scannedPaths, containsAll([previewPath, metaPath])); expect( fakeRawCapture.scannedPaths, isNot(contains('${tempDir.path}/MiniTIAS_QM_20260101_120000.dng')), ); expect(fakeRawCapture.scannedPaths, hasLength(2)); }); test('.png 削除後,その .png パスで scanFile が呼ばれる(連動なし)', () async { final pngPath = '${tempDir.path}/MiniTIAS_20260101_120000.png'; await File(pngPath).create(); final result = await provider.deleteImage(File(pngPath)); expect(result, isTrue); expect(fakeRawCapture.scannedPaths, [pngPath]); }); test('scanFile が例外を投げても削除は成功扱いになる', () async { final pngPath = '${tempDir.path}/MiniTIAS_20260101_120000.png'; await File(pngPath).create(); fakeRawCapture.scanShouldThrow = true; final result = await provider.deleteImage(File(pngPath)); expect(result, isTrue); expect(await File(pngPath).exists(), isFalse); }); }); }