Newer
Older
SmTIAS-Capture / test / providers / gallery_provider_test.dart
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 {
      // 先に古いファイルを作成し,十分な時間差を設けてから新しいファイルを作成する
      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<void>.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);
    });
  });
}