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<String> scannedPaths = [];
bool scanShouldThrow = false;
@override
Future<void> 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 {
// ソートキーは statSync().modified(更新日時).実スリープに頼らず
// 各ファイルの mtime を明示設定して決定的・即時に時間差を作る.
final olderFile = File('${tempDir.path}/MiniTIAS_20260101_120000.png');
final newerFile = File(
'${tempDir.path}/MiniTIAS_QM_20260101_120001.preview.jpg',
);
await olderFile.create();
await newerFile.create();
// NTFS 等の mtime 精度より十分大きい 1 秒差を明示的に与える
olderFile.setLastModifiedSync(DateTime(2026, 1, 1, 12, 0, 0));
newerFile.setLastModifiedSync(DateTime(2026, 1, 1, 12, 0, 1));
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);
});
});
}